Comment: | Merge dev branch |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | payment |
Files: | files | file ages | folders |
SHA3-256: |
0bc6030888babc729cbd1c70162afc0c |
User & Date: | alinaar on 2023-06-05 14:19:19 |
Other Links: | branch diff | manifest | tags |
2023-06-05
| ||
14:19 | Merge dev branch Leaf check-in: 0bc6030888 user: alinaar tags: payment | |
14:17 | Fix payment without registered author check-in: e2bd902d18 user: alinaar tags: payment | |
10:28 | Make sure to cast the column when copying the table data, so that if by error a string is present for an integer, this will not break the app Leaf check-in: 14d6d002a7 user: bohwaz tags: dev | |
Modified doc/admin/brindille_functions.md from [910660e9b2] to [002be74ad6].
︙ | ︙ | |||
146 147 148 149 150 151 152 | ## http Permet de modifier les entêtes HTTP renvoyés par la page. Cette fonction doit être appelée au tout début du squelette, avant tout autre code ou ligne vide. | Paramètre | Optionnel / obligatoire ? | Fonction | | :- | :- | :- | | `code` | *optionnel* | Modifie le code HTTP renvoyé. [Liste des codes HTTP](https://fr.wikipedia.org/wiki/Liste_des_codes_HTTP) | | | > | 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | ## http Permet de modifier les entêtes HTTP renvoyés par la page. Cette fonction doit être appelée au tout début du squelette, avant tout autre code ou ligne vide. | Paramètre | Optionnel / obligatoire ? | Fonction | | :- | :- | :- | | `code` | *optionnel* | Modifie le code HTTP renvoyé. [Liste des codes HTTP](https://fr.wikipedia.org/wiki/Liste_des_codes_HTTP) | | `redirect` | *optionnel* | Rediriger vers l'adresse URL indiquée en valeur. | | `type` | *optionnel* | Modifie le type MIME renvoyé | | `download` | *optionnel* | Force la page à être téléchargée sous le nom indiqué. | | `inline` | *optionnel* | Force la page à être affichée, et peut ensuite être téléchargée sous le nom indiqué (utile pour la généraion de PDF : permet d'afficher le PDF dans le navigateur avant de le télécharger). | Note : si le type `application/pdf` est indiqué (ou juste `pdf`), la page sera convertie en PDF à la volée. Il est possible de forcer le téléchargement du fichier en utilisant le paramètre `download`. Exemples : ``` {{:http code=404}} {{:http redirect="/Nos-Activites/"}} {{:http redirect="https://mon-site-web.tld/"}} {{:http type="application/svg+xml"}} {{:http type="pdf" download="liste_membres_ca.pdf"}} ``` ## include Permet d'inclure un autre squelette. |
︙ | ︙ | |||
328 329 330 331 332 333 334 | </form> ``` ## redirect Redirige vers une nouvelle page. | | > > > | | 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 | </form> ``` ## redirect Redirige vers une nouvelle page. Avec le paramètre `force`, si la page actuelle est ouverte dans une fenêtre modale (grâce à la cible `_dialog`), alors la fenêtre modale est fermée, et la redirection se passe dans la page parente. Avec le paramètre `to`, si la page actuelle est ouverte dans une fenêtre modal (grâce à la cible `_dialog`), alors la fenêtre modale est fermée, et la page parente est rechargée. Si la page n'est pas ouvertre dans dans une fenêtre modale, la redirection est effectuée. Seules les adresses internes sont acceptées, il n'est pas possible de rediriger vers une adresse extérieure. | Paramètre | Obligatoire ou optionnel ? | Fonction | | :- | :- | :- | | `force` | optionnel | Adresse de redirection forcée | | `to` | optionnel | Adresse de redirection si pas dans une fenêtre modale | Si `to=null` est utilisé, alors la fenêtre modale sera fermée. Ou, si la page n'est pas dans une fenêtre modale, la page courante sera rechargée. # Fonctions relatives aux Modules ## save |
︙ | ︙ |
Modified doc/admin/brindille_sections.md from [a1ed96afd1] to [e15d05f9ab].
︙ | ︙ | |||
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 | Liste les membres. Paramètres possibles : | `id` | optionnel | Identifiant unique du membre, ou tableau contenant une liste d'identifiants. | | `search_name` | optionnel | Ne lister que les membres dont le nom correspond au texte passé en paramètre. | Chaque itération renverra la fiche du membre, ainsi que ces variables : | `$id` | Identifiant unique du membre | | `$_name` | Nom du membre, tel que défini dans la configuration | | `$_login` | Identifiant de connexion du membre, tel que défini dans la configuration | | `$_number` | Numéro du membre, tel que défini dans la configuration | ## subscriptions Liste les inscriptions à une ou des activités. Paramètres possibles : | `user` | optionnel | Identifiant unique du membre | | `active` | optionnel | Si `TRUE`, seules les inscriptions à jour sont listées | | `id_service` | optionnel | Ne renvoie que les inscriptions à l'activité correspondant à cet ID. | # Comptabilité ## accounts | > > > | 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 | Liste les membres. Paramètres possibles : | `id` | optionnel | Identifiant unique du membre, ou tableau contenant une liste d'identifiants. | | `search_name` | optionnel | Ne lister que les membres dont le nom correspond au texte passé en paramètre. | | `id_parent` | optionnel | Ne lister que les membres rattachés à l'identifiant unique du membre responsable indiqué. | Chaque itération renverra la fiche du membre, ainsi que ces variables : | `$id` | Identifiant unique du membre | | `$_name` | Nom du membre, tel que défini dans la configuration | | `$_login` | Identifiant de connexion du membre, tel que défini dans la configuration | | `$_number` | Numéro du membre, tel que défini dans la configuration | ## subscriptions Liste les inscriptions à une ou des activités. Paramètres possibles : | Paramètre | | Fonction | | :- | :- | :- | | `user` | optionnel | Identifiant unique du membre | | `active` | optionnel | Si `TRUE`, seules les inscriptions à jour sont listées | | `id_service` | optionnel | Ne renvoie que les inscriptions à l'activité correspondant à cet ID. | # Comptabilité ## accounts |
︙ | ︙ | |||
271 272 273 274 275 276 277 | ## balances Renvoie la balance des comptes. | Paramètre | Fonction | | :- | :- | | `codes` (optionel) | Ne renvoyer que les balances des comptes ayant ces codes (séparer par des virgules). | | | > > > > > > > > > | 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 | ## balances Renvoie la balance des comptes. | Paramètre | Fonction | | :- | :- | | `codes` (optionel) | Ne renvoyer que les balances des comptes ayant ces codes (séparer par des virgules). | | `year` (optionel) | Ne renvoyer que les balances des comptes utilisés sur l'année (indiquer ici un ID de year). | ## transactions Renvoie des écritures. | Paramètre | | Fonction | | :- | :- | :- | | `id` | optionnel | Indiquer un ID d'écriture pour récupérer ses informations. | | `user` | optionnel | Indiquer ici un ID utilisateur pour lister les écritures liées à un membre. | ## years Liste les exercices comptables | Paramètre | Fonction | | :- | :- | |
︙ | ︙ |
Modified doc/admin/markdown.md from [3957587aef] to [aba3e0f63e].
︙ | ︙ | |||
414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 | Il est aussi possible d'utiliser la syntaxe avec des paramètres nommés : ``` <<image file="Nom_fichier.jpg" align="center" caption="Légende">> ``` Les images qui ne sont pas mentionnées dans le texte seront affichées après le texte sous forme de galerie. ## Fichiers joints Pour créer un bouton permettant de voir ou télécharger un fichier joint à la page web, il suffit d'utiliser la syntaxe suivante : ``` <<file|Nom_fichier.ext|Libellé>> ``` * `Nom_fichier.ext` : remplacer par le nom du fichier (parmi les fichiers joints à la page) * `Libellé` : indique le libellé du qui sera affiché sur le bouton, si aucun libellé n'est indiqué alors c'est le nom du fichier qui sera affiché ## Sommaire / table des matières automatique Il est possible de placer le code `<<toc>>` pour générer un sommaire automatiquement à partir des titres et sous-titres : ``` <<toc>> | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 | Il est aussi possible d'utiliser la syntaxe avec des paramètres nommés : ``` <<image file="Nom_fichier.jpg" align="center" caption="Légende">> ``` Les images qui ne sont pas mentionnées dans le texte seront affichées après le texte sous forme de galerie. ## Galerie d'images Il est possible d'afficher une galerie d'images (sous forme d'images miniatures) avec la balise `<<gallery` qui contient la liste des images à mettre dans la galerie : ``` <<gallery Nom_fichier.jpg Nom_fichier_2.jpg >> ``` Si aucun nom de fichier n'est indiqué, alors toutes les images jointes à la page seront affichées : ``` <<gallery>> ``` ### Diaporama d'images On peut également afficher cette galerie sous forme de diaporama. Dans ce cas une seule image est affichée, et on peut passer de l'une à l'autre. La syntaxe est la même, mais on ajoute le mot `slideshow` après le mot `gallery` : ``` <<gallery slideshow Nom_fichier.jpg Nom_fichier_2.jpg >> ``` ## Fichiers joints Pour créer un bouton permettant de voir ou télécharger un fichier joint à la page web, il suffit d'utiliser la syntaxe suivante : ``` <<file|Nom_fichier.ext|Libellé>> ``` * `Nom_fichier.ext` : remplacer par le nom du fichier (parmi les fichiers joints à la page) * `Libellé` : indique le libellé du qui sera affiché sur le bouton, si aucun libellé n'est indiqué alors c'est le nom du fichier qui sera affiché ## Vidéos Pour inclure un lecteur vidéo dans la page web à partir d'un fichier vidéo joint à la page, il faut utiliser le code suivant : ``` <<video|Nom_du_fichier.ext>> ``` On peut aussi spécifier d'autres paramètres : * `file` : nom du fichier vidéo * `poster` : nom de fichier d'une image utilisée pour remplacer la vidéo avant qu'elle ne soit lue * `subtitles` : nom d'un fichier de sous-titres au format VTT (le format SRT n'est pas géré par les navigateurs) * `width` : largeur de la vidéo (en pixels) * `height` : hauteur de la vidéo (en pixels) Exemple : ``` <<video file="Ma_video.webm" poster="Ma_video_poster.jpg" width="640" height="360" subtitles="Ma_video_sous_titres.vtt">> ``` ## Sommaire / table des matières automatique Il est possible de placer le code `<<toc>>` pour générer un sommaire automatiquement à partir des titres et sous-titres : ``` <<toc>> |
︙ | ︙ |
Modified src/Makefile from [64ffb982f0] to [5d53242d18].
︙ | ︙ | |||
44 45 46 47 48 49 50 | mv www/admin/static/mini.css /tmp/paheko-build/paheko/src/www/admin/static/admin.css # Generate .htaccess file cd /tmp/paheko-build/paheko/src && make htaccess cd /tmp/paheko-build/paheko/src/www/admin/static; \ rm -f font/*.css font/*.json cd /tmp/paheko-build/paheko/src; \ rm -f Makefile include/lib/KD2/data/countries.en.json | | | | | | | | | | 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | mv www/admin/static/mini.css /tmp/paheko-build/paheko/src/www/admin/static/admin.css # Generate .htaccess file cd /tmp/paheko-build/paheko/src && make htaccess cd /tmp/paheko-build/paheko/src/www/admin/static; \ rm -f font/*.css font/*.json cd /tmp/paheko-build/paheko/src; \ rm -f Makefile include/lib/KD2/data/countries.en.json #cd /tmp/paheko-build/paheko/src/data; mkdir plugins && cd plugins; \ # wget https://fossil.kd2.org/paheko-plugins/uv/welcome.tar.gz \ # && wget https://fossil.kd2.org/paheko-plugins/uv/caisse.tar.gz \ # && wget https://fossil.kd2.org/paheko-plugins/uv/taima.tar.gz \ # && wget https://fossil.kd2.org/paheko-plugins/uv/dompdf.tar.gz \ # && wget https://fossil.kd2.org/paheko-plugins/uv/reservations.tar.gz \ # && wget https://fossil.kd2.org/paheko-plugins/uv/webstats.tar.gz \ # && wget https://fossil.kd2.org/paheko-plugins/uv/stock_velos.tar.gz mv /tmp/paheko-build/paheko/src /tmp/paheko-build/paheko-${VERSION} @#cd /tmp/paheko-build/; zip -r -9 paheko-${VERSION}.zip paheko-${VERSION}; @#mv -f /tmp/paheko-build/paheko-${VERSION}.zip ./ tar czvfh paheko-${VERSION}.tar.gz --hard-dereference -C /tmp/paheko-build paheko-${VERSION} deb: cd ../build/debian; ./makedeb.sh |
︙ | ︙ |
Modified src/include/lib/Garradin/Accounting/Reports.php from [0f7acbbe10] to [be49fc6e79].
︙ | ︙ | |||
599 600 601 602 603 604 605 | $account->all_debit = $debit; $account->all_credit = $credit; yield $account; } | | | | 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 | $account->all_debit = $debit; $account->all_credit = $credit; yield $account; } static public function getJournal(array $criterias, bool $reverse_order = false): \Generator { $where = self::getWhereClause($criterias, 't', 'l', 'a'); $sql = sprintf('SELECT t.id_year, l.id_account, l.debit, l.credit, t.id, t.date, t.reference, l.reference AS line_reference, t.label, l.label AS line_label, a.label AS account_label, a.code AS account_code FROM acc_transactions t INNER JOIN acc_transactions_lines l ON l.id_transaction = t.id INNER JOIN acc_accounts a ON l.id_account = a.id WHERE %s ORDER BY t.date %s, t.id %2$s;', $where, $reverse_order ? 'DESC' : 'ASC'); $transaction = null; $db = DB::getInstance(); foreach ($db->iterate($sql) as $row) { if (null !== $transaction && $transaction->id != $row->id) { yield $transaction; |
︙ | ︙ |
Modified src/include/lib/Garradin/AdvancedSearch.php from [5d8ccf1b9e] to [b6223d2681].
︙ | ︙ | |||
75 76 77 78 79 80 81 82 83 84 85 86 87 88 | } DB::getInstance()->toggleUnicodeLike(true); $list = new DynamicList($select_columns, $tables, $conditions->where); $list->orderBy($order, $query->desc ?? $default_desc); return $list; } /** * Redirects to a URL if only one result is found for a simple search */ public function redirect(DynamicList $list): void | > | 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | } DB::getInstance()->toggleUnicodeLike(true); $list = new DynamicList($select_columns, $tables, $conditions->where); $list->orderBy($order, $query->desc ?? $default_desc); $list->setTitle('Recherche'); return $list; } /** * Redirects to a URL if only one result is found for a simple search */ public function redirect(DynamicList $list): void |
︙ | ︙ |
Modified src/include/lib/Garradin/Config.php from [38db93d71b] to [9254da223c].
︙ | ︙ | |||
58 59 60 61 62 63 64 65 66 67 68 69 70 71 | protected array $files = []; protected bool $site_disabled; protected int $log_retention; protected bool $analytical_set_all; static protected $_instance = null; static public function getInstance() { return self::$_instance ?: self::$_instance = new self; } | > > | 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | protected array $files = []; protected bool $site_disabled; protected int $log_retention; protected bool $analytical_set_all; protected ?int $auto_logout = 0; static protected $_instance = null; static public function getInstance() { return self::$_instance ?: self::$_instance = new self; } |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Accounting/Project.php from [58624a9c0e] to [66b8f38ddf].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php namespace Garradin\Entities\Accounting; use Garradin\DB; use Garradin\Entity; /** * Analytical projects */ class Project extends Entity { const TABLE = 'acc_projects'; protected ?int $id; protected ?string $code; protected string $label; protected ?string $description; protected bool $archived = false; | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?php namespace Garradin\Entities\Accounting; use Garradin\DB; use Garradin\Entity; /** * Analytical projects */ class Project extends Entity { const NAME = 'Projet analytique'; const TABLE = 'acc_projects'; protected ?int $id; protected ?string $code; protected string $label; protected ?string $description; protected bool $archived = false; |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Email/Mailing.php from [8edd3b7d1b] to [99064c2e70].
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 | <?php namespace Garradin\Entities\Email; use Garradin\Config; use Garradin\CSV; use Garradin\DB; use Garradin\DynamicList; use Garradin\Entity; use Garradin\UserException; use Garradin\Email\Emails; use Garradin\Users\DynamicFields; use Garradin\Users\Users; use Garradin\UserTemplate\UserTemplate; use Garradin\Web\Render\Render; use DateTime; use stdClass; class Mailing extends Entity { const TABLE = 'mailings'; protected ?int $id = null; protected string $subject; protected ?string $body; /** * Leave sender name and email NULL to use org name + email | > > > | 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 | <?php namespace Garradin\Entities\Email; use Garradin\Config; use Garradin\CSV; use Garradin\DB; use Garradin\DynamicList; use Garradin\Entity; use Garradin\Log; use Garradin\UserException; use Garradin\Email\Emails; use Garradin\Users\DynamicFields; use Garradin\Users\Users; use Garradin\UserTemplate\UserTemplate; use Garradin\Web\Render\Render; use DateTime; use stdClass; class Mailing extends Entity { const TABLE = 'mailings'; const NAME = 'Message collectif'; const PRIVATE_URL = '!users/mailing/details.php?id=%d'; protected ?int $id = null; protected string $subject; protected ?string $body; /** * Leave sender name and email NULL to use org name + email |
︙ | ︙ | |||
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 | $this->getBody(), Render::FORMAT_MARKDOWN ); $this->set('sent', new DateTime); $this->save(); } public function export(string $format): void { $rows = []; foreach ($this->listRecipients() as $row) { $rows[] = [$row->email ?? '(Anonymisée)', $row->name]; } CSV::export($format, 'Destinataires message collectif', $rows, ['Adresse e-mail', 'Identité']); } } | > > | 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 | $this->getBody(), Render::FORMAT_MARKDOWN ); $this->set('sent', new DateTime); $this->save(); Log::add(Log::SENT, ['entity' => get_class($this), 'id' => $this->id()]); } public function export(string $format): void { $rows = []; foreach ($this->listRecipients() as $row) { $rows[] = [$row->email ?? '(Anonymisée)', $row->name]; } CSV::export($format, 'Destinataires message collectif', $rows, ['Adresse e-mail', 'Identité']); } } |
Modified src/include/lib/Garradin/Entities/Files/File.php from [eb5ed83d98] to [d76fee0a10].
︙ | ︙ | |||
74 75 76 77 78 79 80 | const ALLOWED_THUMB_SIZES = [ '150px' => [['resize', 150]], '200px' => [['resize', 200]], '500px' => [['resize', 500]], 'crop-256px' => [['cropResize', 256, 256]], ]; | | | 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | const ALLOWED_THUMB_SIZES = [ '150px' => [['resize', 150]], '200px' => [['resize', 200]], '500px' => [['resize', 500]], 'crop-256px' => [['cropResize', 256, 256]], ]; const THUMB_CACHE_ID = 'file.thumb.%s.%s'; const THUMB_SIZE_TINY = '200px'; const THUMB_SIZE_SMALL = '500px'; const CONTEXT_DOCUMENTS = 'documents'; const CONTEXT_USER = 'user'; const CONTEXT_TRANSACTION = 'transaction'; |
︙ | ︙ | |||
319 320 321 322 323 324 325 326 327 328 329 330 331 332 | $escaped = strtr($this->path, ['%' => '!%', '_' => '!_', '!' => '!!']); // Rename references in files_search DB::getInstance()->preparedQuery('UPDATE files_search SET path = ? || SUBSTR(path, 1+LENGTH(?)) WHERE path LIKE ?;', $new_path . '/', $this->path . '/', $escaped . '%'); return $return; } /** * Copy the current file to a new location * @param string $target Target path | > > > | 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 | $escaped = strtr($this->path, ['%' => '!%', '_' => '!_', '!' => '!!']); // Rename references in files_search DB::getInstance()->preparedQuery('UPDATE files_search SET path = ? || SUBSTR(path, 1+LENGTH(?)) WHERE path LIKE ?;', $new_path . '/', $this->path . '/', $escaped . '%'); $this->set('parent', Utils::dirname($new_path)); $this->set('path', $new_path); return $return; } /** * Copy the current file to a new location * @param string $target Target path |
︙ | ︙ | |||
782 783 784 785 786 787 788 789 790 | } elseif ($content = Files::callStorage('fetch', $this)) { $i = Image::createFromBlob($content); } else { throw new \RuntimeException('Unable to fetch file'); } $operations = self::ALLOWED_THUMB_SIZES[$size]; | > > > | > > > > > > | | 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 | } elseif ($content = Files::callStorage('fetch', $this)) { $i = Image::createFromBlob($content); } else { throw new \RuntimeException('Unable to fetch file'); } // Always autorotate first $i->autoRotate(); $operations = self::ALLOWED_THUMB_SIZES[$size]; $allowed_operations = ['resize', 'cropResize', 'flip', 'rotate', 'crop']; foreach ($operations as $operation) { $arguments = array_slice($operation, 1); $operation = $operation[0]; if (!in_array($operation, $allowed_operations)) { throw new \InvalidArgumentException('Opération invalide: ' . $operation); } call_user_func_array([$i, $operation], $arguments); } $format = null; if ($i->format() !== 'gif') { $format = ['webp', null]; } $i->save($destination, $format); } catch (\RuntimeException $e) { throw new UserException('Impossible de créer la miniature'); } } $this->_serve($destination, null); |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Module.php from [82fcb8989b] to [aca5a90182].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <?php namespace Garradin\Entities; use Garradin\Entity; use Garradin\DB; use Garradin\Plugins; use Garradin\UserException; use Garradin\Files\Files; use Garradin\UserTemplate\UserTemplate; use Garradin\Users\Session; use Garradin\Web\Cache; use Garradin\Entities\Files\File; use const Garradin\{ROOT, WWW_URL}; class Module extends Entity { const ROOT = File::CONTEXT_MODULES; const DIST_ROOT = ROOT . '/modules'; const META_FILE = 'module.ini'; const ICON_FILE = 'icon.svg'; | > < > > > > > | 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 | <?php namespace Garradin\Entities; use Garradin\Entity; use Garradin\DB; use Garradin\Plugins; use Garradin\UserException; use Garradin\Files\Files; use Garradin\UserTemplate\UserTemplate; use Garradin\Users\Session; use Garradin\Web\Cache; use Garradin\Web\Router; use Garradin\Entities\Files\File; use const Garradin\{ROOT, WWW_URL}; class Module extends Entity { const ROOT = File::CONTEXT_MODULES; const DIST_ROOT = ROOT . '/modules'; const META_FILE = 'module.ini'; const ICON_FILE = 'icon.svg'; const CONFIG_FILE = 'config.html'; const INDEX_FILE = 'index.html'; const README_FILE = 'README.md'; // Snippets, don't forget to create alias constant in UserTemplate\Modules class const SNIPPET_TRANSACTION = 'snippets/transaction_details.html'; const SNIPPET_USER = 'snippets/user_details.html'; const SNIPPET_HOME_BUTTON = 'snippets/home_button.html'; const SNIPPET_MY_SERVICES = 'snippets/my_services.html'; const SNIPPET_MY_DETAILS = 'snippets/my_details.html'; const SNIPPETS = [ self::SNIPPET_HOME_BUTTON => 'Icône sur la page d\'accueil', self::SNIPPET_USER => 'En bas de la fiche d\'un membre', self::SNIPPET_TRANSACTION => 'En bas de la fiche d\'une écriture', self::SNIPPET_MY_SERVICES => 'Page "Mes activités"', self::SNIPPET_MY_DETAILS => 'Page "Mes infos personnelles"', ]; const VALID_NAME_REGEXP = '/^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/'; const TABLE = 'modules'; protected ?int $id; |
︙ | ︙ | |||
320 321 322 323 324 325 326 | 'dist' => true, 'file_path' => $base . $path . '/' . $file, ]; } } foreach ($out as &$file) { | | | | 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 | 'dist' => true, 'file_path' => $base . $path . '/' . $file, ]; } } foreach ($out as &$file) { $file['editable'] = !$file['dir'] && (UserTemplate::isTemplate($file['path']) || substr($file['type'], 0, 5) === 'text/' || preg_match('/\.(?:json|md|skriv|html|css|js|ini)$/', $file['name'])); $file['open_url'] = '!common/files/preview.php?p=' . rawurlencode($file['file_path']); $file['edit_url'] = '!common/files/edit.php?p=' . rawurlencode($file['file_path']); $file['delete_url'] = '!common/files/delete.php?p=' . rawurlencode($file['file_path']); } unset($file); |
︙ | ︙ | |||
428 429 430 431 432 433 434 435 436 437 438 439 440 441 | $this->serveWeb($path, $params); return; } else { $ut = $this->template($path); $ut->serve($params); } } // Serve a static file from a user module elseif ($has_local_file) { $file = Files::get(File::CONTEXT_MODULES . '/' . $this->name . '/' . $path); if (!$file) { throw new UserException('Invalid path'); | > > > > > > > > > > > > > > > > > > > | 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 | $this->serveWeb($path, $params); return; } else { $ut = $this->template($path); $ut->serve($params); } return; } // Render a markdown file elseif (substr($path, -3) === '.md') { if ($has_local_file) { $file = Files::get(File::CONTEXT_MODULES . '/' . $this->name . '/' . $path); if (!$file) { throw new UserException('Invalid path'); } $text = $file->fetch(); } else { $text = @file_get_contents($this->distPath($path)); } Router::markdown($text); } // Serve a static file from a user module elseif ($has_local_file) { $file = Files::get(File::CONTEXT_MODULES . '/' . $this->name . '/' . $path); if (!$file) { throw new UserException('Invalid path'); |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Plugin.php from [03a3f05ec0] to [b92e8bee25].
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?php namespace Garradin\Entities; use Garradin\Entity; use Garradin\DB; use Garradin\Plugins; use Garradin\Template; use Garradin\UserException; use Garradin\Utils; use Garradin\Files\Files; use Garradin\UserTemplate\UserTemplate; use Garradin\Users\Session; | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?php namespace Garradin\Entities; use Garradin\Entity; use Garradin\DB; use Garradin\Plugins; use Garradin\Template; use Garradin\UserException; use Garradin\Utils; use Garradin\Files\Files; use Garradin\UserTemplate\UserTemplate; use Garradin\Users\Session; use Garradin\Web\Router; use Garradin\Entities\Files\File; use const Garradin\{PLUGINS_ROOT, WWW_URL, ROOT, ADMIN_URL}; class Plugin extends Entity { |
︙ | ︙ | |||
111 112 113 114 115 116 117 118 119 120 121 122 123 124 | } /** * Fills information from plugin.ini file */ public function updateFromINI(): bool { $ini = parse_ini_file($this->path(self::META_FILE), false, \INI_SCANNER_TYPED); if (empty($ini)) { return false; } $ini = (object) $ini; | > > > > | 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | } /** * Fills information from plugin.ini file */ public function updateFromINI(): bool { if (!$this->hasFile(self::META_FILE)) { return false; } $ini = parse_ini_file($this->path(self::META_FILE), false, \INI_SCANNER_TYPED); if (empty($ini)) { return false; } $ini = (object) $ini; |
︙ | ︙ | |||
365 366 367 368 369 370 371 | $tpl->assign('plugin_root', \Garradin\PLUGIN_ROOT); } $plugin = $this; include $path; } | | < < < < < < | | 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 | $tpl->assign('plugin_root', \Garradin\PLUGIN_ROOT); } $plugin = $this; include $path; } elseif (substr($file, -3) === '.md') { Router::markdown(file_get_contents($path)); } else { // Récupération du type MIME à partir de l'extension $pos = strrpos($path, '.'); $ext = substr($path, $pos+1); $mime = self::MIME_TYPES[$ext] ?? 'text/plain'; |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Search.php from [3bf6c6ad87] to [963f668a8c].
︙ | ︙ | |||
56 57 58 59 60 61 62 | $this->assert(strlen('label') > 0, 'Le champ libellé doit être renseigné'); $this->assert(strlen('label') <= 500, 'Le champ libellé est trop long'); $db = DB::getInstance(); if ($this->id_user !== null) { | | | 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | $this->assert(strlen('label') > 0, 'Le champ libellé doit être renseigné'); $this->assert(strlen('label') <= 500, 'Le champ libellé est trop long'); $db = DB::getInstance(); if ($this->id_user !== null) { $this->assert($db->test('users', 'id = ?', $this->id_user), 'Numéro de membre inconnu'); } $this->assert(array_key_exists($this->type, self::TYPES)); $this->assert(array_key_exists($this->target, self::TARGETS)); $this->assert(strlen($this->content), 'Le contenu de la recherche ne peut être vide'); |
︙ | ︙ | |||
123 124 125 126 127 128 129 | $sql = $this->content; } $has_limit = preg_match('/LIMIT\s+\d+/i', $sql); // force LIMIT if (!empty($options['limit'])) { | | | 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | $sql = $this->content; } $has_limit = preg_match('/LIMIT\s+\d+/i', $sql); // force LIMIT if (!empty($options['limit'])) { $sql = preg_replace($has_limit ? '/LIMIT\s+.*$/is' : '/;.*$/s', '', trim($sql)); $sql .= ' LIMIT ' . (int) $options['limit']; } elseif (!empty($options['no_limit']) && $has_limit) { $sql = preg_replace('/LIMIT\s+.*;?\s*$/', '', $sql); } if (!empty($options['select_also'])) { |
︙ | ︙ | |||
169 170 171 172 173 174 175 176 177 178 179 180 181 182 | if (empty($options['no_cache'])) { $this->_result = $result; } return $result; } catch (DB_Exception $e) { throw new UserException('Erreur dans la requête : ' . $e->getMessage(), 0, $e); } finally { $db->toggleUnicodeLike(false); } } | > | 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | if (empty($options['no_cache'])) { $this->_result = $result; } return $result; } catch (DB_Exception $e) { throw $e; throw new UserException('Erreur dans la requête : ' . $e->getMessage(), 0, $e); } finally { $db->toggleUnicodeLike(false); } } |
︙ | ︙ | |||
205 206 207 208 209 210 211 | { $sql = $this->SQL(); if (!preg_match('/(?:FROM|JOIN)\s+users/i', $sql)) { return false; } | | | 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | { $sql = $this->SQL(); if (!preg_match('/(?:FROM|JOIN)\s+users/i', $sql)) { return false; } $header = $this->getHeader(['limit' => 1, 'no_cache' => true]); if (!in_array('id', $header) && !in_array('_user_id', $header)) { return false; } return true; } |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Services/Fee.php from [50d2be14e4] to [13cbc5b2de].
︙ | ︙ | |||
108 109 110 111 112 113 114 | } return null; } protected function getFormulaSQL() { | | | | 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | } return null; } protected function getFormulaSQL() { return sprintf('SELECT (%s) FROM users WHERE id = ?;', $this->formula); } protected function checkFormula(): ?string { try { $db = DB::getInstance(); $sql = $this->getFormulaSQL(); $db->protectSelect(['users' => null, 'services_users' => null, 'services' => null, 'services_fees' => null], $sql); return null; } catch (DB_Exception $e) { return $e->getMessage(); } } |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Services/Reminder.php from [5eb7d7e44e] to [897f767e6b].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?php namespace Garradin\Entities\Services; use Garradin\DynamicList; use Garradin\Entity; use Garradin\ValidationException; use Garradin\Users\DynamicFields; use KD2\DB\EntityManager; class Reminder extends Entity { const TABLE = 'services_reminders'; protected $id; protected $id_service; protected $delay; protected $subject; protected $body; | > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <?php namespace Garradin\Entities\Services; use Garradin\DynamicList; use Garradin\DB; use Garradin\Entity; use Garradin\ValidationException; use Garradin\Users\DynamicFields; use KD2\DB\EntityManager; class Reminder extends Entity { const NAME = 'Rappel'; const TABLE = 'services_reminders'; protected $id; protected $id_service; protected $delay; protected $subject; protected $body; |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Users/DynamicField.php from [9c8197c3d0] to [1fff10af23].
︙ | ︙ | |||
112 113 114 115 116 117 118 | 'month' => '?string', 'year' => '?int', 'file' => '?string', 'password' => '?string', 'number' => '?int|float', 'tel' => '?string', 'select' => '?string', | | | | 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 139 140 141 142 143 144 145 146 | 'month' => '?string', 'year' => '?int', 'file' => '?string', 'password' => '?string', 'number' => '?int|float', 'tel' => '?string', 'select' => '?string', 'multiple' => '?int', 'country' => '?string', 'text' => '?string', 'textarea' => '?string', 'generated'=> 'dynamic', ]; const SQL_TYPES = [ 'email' => 'TEXT', 'url' => 'TEXT', 'checkbox' => 'INTEGER NOT NULL DEFAULT 0', 'date' => 'TEXT', 'datetime' => 'TEXT', 'month' => 'TEXT', 'year' => 'INTEGER', 'file' => 'TEXT', 'password' => 'TEXT', 'number' => 'INTEGER', 'tel' => 'TEXT', 'select' => 'TEXT', 'multiple' => 'INTEGER', 'country' => 'TEXT', 'text' => 'TEXT', 'textarea' => 'TEXT', 'generated'=> 'GENERATED', ]; const SEARCH_TYPES = [ |
︙ | ︙ | |||
192 193 194 195 196 197 198 199 200 201 202 203 204 205 | 'date_updated TEXT NULL CHECK (date_updated IS NULL OR datetime(date_updated) = date_updated),', 'otp_secret TEXT NULL,', 'pgp_key TEXT NULL,', 'id_parent INTEGER NULL REFERENCES users(id) ON DELETE SET NULL CHECK (id_parent IS NULL OR is_parent = 0),', 'is_parent INTEGER NOT NULL DEFAULT 0,', 'preferences TEXT NULL,' ]; public function delete(): bool { if (!$this->canDelete()) { throw new ValidationException('Ce champ est utilisé en interne, il n\'est pas possible de le supprimer'); } | > > > > > > > > > | 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 | 'date_updated TEXT NULL CHECK (date_updated IS NULL OR datetime(date_updated) = date_updated),', 'otp_secret TEXT NULL,', 'pgp_key TEXT NULL,', 'id_parent INTEGER NULL REFERENCES users(id) ON DELETE SET NULL CHECK (id_parent IS NULL OR is_parent = 0),', 'is_parent INTEGER NOT NULL DEFAULT 0,', 'preferences TEXT NULL,' ]; public function sql_type(): string { if ($this->type == 'checkbox') { return 'INTEGER'; } return self::SQL_TYPES[$this->type]; } public function delete(): bool { if (!$this->canDelete()) { throw new ValidationException('Ce champ est utilisé en interne, il n\'est pas possible de le supprimer'); } |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Users/User.php from [6a62a114e5] to [699fa88d94].
︙ | ︙ | |||
319 320 321 322 323 324 325 | $v = 0; foreach (array_keys($source[$f->name] ?? []) as $k) { $k = 0x01 << $k; $v |= $k; } | | | 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 | $v = 0; foreach (array_keys($source[$f->name] ?? []) as $k) { $k = 0x01 << $k; $v |= $k; } $source[$f->name] = $v ?: null; } return parent::importForm($source); } public function importSecurityForm(bool $user_mode = true, array $source = null) { |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Web/Page.php from [304428ee4a] to [6248ab559a].
︙ | ︙ | |||
62 63 64 65 66 67 68 | const TEMPLATES = [ self::TYPE_PAGE => 'article.html', self::TYPE_CATEGORY => 'category.html', ]; const DUPLICATE_URI_ERROR = 42; | | | | > > | 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | const TEMPLATES = [ self::TYPE_PAGE => 'article.html', self::TYPE_CATEGORY => 'category.html', ]; const DUPLICATE_URI_ERROR = 42; protected ?File $_file = null; protected ?array $_attachments = null; protected ?array $_tagged_attachments = null; protected ?string $_html = null; protected ?\DateTime $_html_modified = null; static public function create(int $type, ?string $parent, string $title, string $status = self::STATUS_ONLINE): self { $page = new self; $data = compact('type', 'parent', 'title', 'status'); $data['content'] = ''; |
︙ | ︙ | |||
134 135 136 137 138 139 140 | public function render(?string $user_prefix = null): string { if (!$this->file()) { throw new \LogicException('File does not exist: ' . $this->file_path); } | > > > > | > > > | 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | public function render(?string $user_prefix = null): string { if (!$this->file()) { throw new \LogicException('File does not exist: ' . $this->file_path); } if ($this->_html_modified != $this->file()->modified) { $this->_html_modified = $this->_html = null; } $this->_html ??= Render::render($this->format, $this->file(), $this->content, $user_prefix); $this->_html_modified ??= $this->file()->modified; return $this->_html; } public function excerpt(int $length = 600): string { return $this->preview(mb_substr($this->content, 0, $length) . "\n\n…"); } |
︙ | ︙ |
Modified src/include/lib/Garradin/Entity.php from [620eaeef49] to [08bdf3b889].
︙ | ︙ | |||
79 80 81 82 83 84 85 | if ($value instanceof Date) { return $value; } elseif ($value instanceof \DateTimeInterface) { return Date::createFromInterface($value); } | | > > > > > > > > > > > > | 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 | if ($value instanceof Date) { return $value; } elseif ($value instanceof \DateTimeInterface) { return Date::createFromInterface($value); } $d = self::filterUserDateValue($value); if (!$d) { return $d; } $y = $d->format('Y'); if ($y < 1900 || $y > 2100) { throw new ValidationException('Date invalide: doit être entre 1900 et 2100'); } return $d; } elseif ($type == 'DateTime' && is_string($value)) { if (preg_match('!^\d{2}/\d{2}/\d{4}\s\d{1,2}:\d{2}$!', $value)) { return \DateTime::createFromFormat('d/m/Y H:i', $value); } } |
︙ | ︙ | |||
137 138 139 140 141 142 143 | return true; } $return = parent::save(false); // Log creation/edit, but don't record stuff that doesn't change anything if ($this::NAME && ($new || $modified)) { | < | | 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | return true; } $return = parent::save(false); // Log creation/edit, but don't record stuff that doesn't change anything if ($this::NAME && ($new || $modified)) { Log::add($new ? Log::CREATE : Log::EDIT, ['entity' => get_class($this), 'id' => $this->id()]); } Plugins::fireSignal($name . '.after', ['entity' => $this, 'success' => $return, 'new' => $new]); Plugins::fireSignal('entity.save.after', ['entity' => $this, 'success' => $return, 'new' => $new]); return $return; |
︙ | ︙ | |||
168 169 170 171 172 173 174 | if (Plugins::fireSignal('entity.delete.before', ['entity' => $this, 'id' => $id])) { return true; } $return = parent::delete(); if ($this::NAME) { | | | 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | if (Plugins::fireSignal('entity.delete.before', ['entity' => $this, 'id' => $id])) { return true; } $return = parent::delete(); if ($this::NAME) { Log::add(Log::DELETE, ['entity' => get_class($this), 'id' => $id]); } Plugins::fireSignal($name . '.after', ['entity' => $this, 'success' => $return, 'id' => $id]); Plugins::fireSignal('entity.delete.after', ['entity' => $this, 'success' => $return, 'id' => $id]); return $return; } } |
Modified src/include/lib/Garradin/Files/Storage/FileSystem.php from [e490f9d582] to [1bae585d73].
︙ | ︙ | |||
105 106 107 108 109 110 111 | static public function touch(string $path, $date = null): bool { if ($date instanceof \DateTimeInterface) { $date = $date->getTimestamp(); } | | | 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | static public function touch(string $path, $date = null): bool { if ($date instanceof \DateTimeInterface) { $date = $date->getTimestamp(); } return touch(self::_getRealPath($path), $date ?? time()); } static protected function _getRealPath(string $path): ?string { if (substr(trim($path, '/'), 0, 1) == '.') { return null; } |
︙ | ︙ |
Modified src/include/lib/Garradin/Install.php from [1d576f102a] to [c7cd9b91ab].
︙ | ︙ | |||
233 234 235 236 237 238 239 240 241 242 243 244 245 246 | $config->import([ 'org_name' => $name, 'org_email' => $user_email, 'currency' => $currency, 'country' => $country_code, 'site_disabled' => true, 'log_retention' => 365, 'analytical_set_all' => true, ]); $fields = DynamicFields::getInstance(); $fields->install(); // Create default category for common users | > | 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 | $config->import([ 'org_name' => $name, 'org_email' => $user_email, 'currency' => $currency, 'country' => $country_code, 'site_disabled' => true, 'log_retention' => 365, 'auto_logout' => 2*60, 'analytical_set_all' => true, ]); $fields = DynamicFields::getInstance(); $fields->install(); // Create default category for common users |
︙ | ︙ |
Modified src/include/lib/Garradin/Log.php from [4abbd78347] to [80e26a18a5].
︙ | ︙ | |||
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 | const LOGIN_PASSWORD_CHANGE = 4; const LOGIN_CHANGE = 5; const LOGIN_AS = 6; const CREATE = 10; const DELETE = 11; const EDIT = 12; const ACTIONS = [ self::LOGIN_FAIL => 'Connexion refusée', self::LOGIN_SUCCESS => 'Connexion réussie', self::LOGIN_RECOVER => 'Mot de passe perdu', self::LOGIN_PASSWORD_CHANGE => 'Modification de mot de passe', self::LOGIN_CHANGE => 'Modification d\'identifiant', self::LOGIN_AS => 'Connexion par un administrateur', self::CREATE => 'Création', self::DELETE => 'Suppression', self::EDIT => 'Modification', ]; static public function add(int $type, ?array $details = null, int $id_user = null): void { if (defined('Garradin\INSTALL_PROCESS')) { return; } if ($type != self::LOGIN_FAIL) { $keep = Config::getInstance()->log_retention; // Don't log anything if ($keep == 0) { return; } } $ip = Utils::getIP(); $session = Session::getInstance(); $id_user ??= Session::getUserId(); DB::getInstance()->insert('logs', [ 'id_user' => $id_user, | > > > > > > | 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 | const LOGIN_PASSWORD_CHANGE = 4; const LOGIN_CHANGE = 5; const LOGIN_AS = 6; const CREATE = 10; const DELETE = 11; const EDIT = 12; const SENT = 13; const ACTIONS = [ self::LOGIN_FAIL => 'Connexion refusée', self::LOGIN_SUCCESS => 'Connexion réussie', self::LOGIN_RECOVER => 'Mot de passe perdu', self::LOGIN_PASSWORD_CHANGE => 'Modification de mot de passe', self::LOGIN_CHANGE => 'Modification d\'identifiant', self::LOGIN_AS => 'Connexion par un administrateur', self::CREATE => 'Création', self::DELETE => 'Suppression', self::EDIT => 'Modification', self::SENT => 'Envoi', ]; static public function add(int $type, ?array $details = null, int $id_user = null): void { if (defined('Garradin\INSTALL_PROCESS')) { return; } if ($type != self::LOGIN_FAIL) { $keep = Config::getInstance()->log_retention; // Don't log anything if ($keep == 0) { return; } } if (isset($details['entity'])) { $details['entity'] = str_replace('Garradin\Entities\\', '', $details['entity']); } $ip = Utils::getIP(); $session = Session::getInstance(); $id_user ??= Session::getUserId(); DB::getInstance()->insert('logs', [ 'id_user' => $id_user, |
︙ | ︙ | |||
163 164 165 166 167 168 169 | $list->orderBy('created', true); $list->setCount('COUNT(logs.id)'); $list->setModifier(function (&$row) { $row->created = \DateTime::createFromFormat('!Y-m-d H:i:s', $row->created); $row->details = $row->details ? json_decode($row->details) : null; $row->type_label = self::ACTIONS[$row->type]; | > > | > > | > > | > > | | 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | $list->orderBy('created', true); $list->setCount('COUNT(logs.id)'); $list->setModifier(function (&$row) { $row->created = \DateTime::createFromFormat('!Y-m-d H:i:s', $row->created); $row->details = $row->details ? json_decode($row->details) : null; $row->type_label = self::ACTIONS[$row->type]; $const = 'Garradin\Entities\\' . $row->details->entity . '::NAME'; if (isset($row->details->entity) && defined($const) && ($value = constant($const))) { $row->entity_name = $value; } $const = 'Garradin\Entities\\' . $row->details->entity . '::PRIVATE_URL'; if (isset($row->details->id, $row->details->entity) && defined($const) && ($value = constant($const))) { $row->entity_url = sprintf($value, $row->details->id); } }); return $list; } } |
Modified src/include/lib/Garradin/Plugins.php from [7e87d31149] to [30dcf74c9a].
︙ | ︙ | |||
167 168 169 170 171 172 173 | foreach ($list as &$item) { $type = isset($item['plugin']) ? 'plugin' : 'module'; $c = $item[$type]; $item = $c->asArray(); $item[$type] = $c; $item['icon_url'] = $c->icon_url(); $item['config_url'] = $c->hasConfig() ? $c->url($c::CONFIG_FILE) : null; | | | 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | foreach ($list as &$item) { $type = isset($item['plugin']) ? 'plugin' : 'module'; $c = $item[$type]; $item = $c->asArray(); $item[$type] = $c; $item['icon_url'] = $c->icon_url(); $item['config_url'] = $c->hasConfig() ? $c->url($c::CONFIG_FILE) : null; $item['readme_url'] = $c->enabled && $c->hasFile($c::README_FILE) ? $c->url($c::README_FILE) : null; $item['installed'] = $type == 'plugin' ? $c->exists() : true; $item['broken'] = $type == 'plugin' ? !$c->hasCode() : false; $item['broken_message'] = $type == 'plugin' ? $c->getBrokenMessage() : false; $item['url'] = null; if ($c->hasFile($c::INDEX_FILE)) { |
︙ | ︙ |
Modified src/include/lib/Garradin/Services/Fees.php from [b73b6d4c72] to [a09b1bea4a].
︙ | ︙ | |||
55 56 57 58 59 60 61 | foreach ($result as &$row) { if (!$row->formula) { continue; } try { | | | 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | foreach ($result as &$row) { if (!$row->formula) { continue; } try { $sql = sprintf('SELECT (%s) FROM users WHERE id = %d;', $row->formula, $user_id); $row->user_amount = $db->firstColumn($sql); } catch (DB_Exception $e) { $row->label .= sprintf(' (**FORMULE DE CALCUL INVALIDE: %s**)', $e->getMessage()); $row->description .= "\n\n**MERCI DE CORRIGER LA FORMULE**"; $row->user_amount = -1; } |
︙ | ︙ |
Modified src/include/lib/Garradin/Template.php from [c7200fc438] to [944f3a8d61].
︙ | ︙ | |||
510 511 512 513 514 515 516 | } else { $required_label = ' <i>(facultatif)</i>'; } $out = sprintf('<dt><label for="f_%s_0">%s</label>%s<input type="hidden" name="%s_present" value="1" /></dt>', $key, htmlspecialchars($field->label), $required_label, $key); | | | | 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 | } else { $required_label = ' <i>(facultatif)</i>'; } $out = sprintf('<dt><label for="f_%s_0">%s</label>%s<input type="hidden" name="%s_present" value="1" /></dt>', $key, htmlspecialchars($field->label), $required_label, $key); if ($field->help ?? null) { $out .= sprintf('<dd class="help">%s</dd>', htmlspecialchars($field->help)); } foreach ($options as $k => $v) { $b = 0x01 << (int)$k; $p = [ |
︙ | ︙ |
Modified src/include/lib/Garradin/UserTemplate/Functions.php from [583c59a6d5] to [8625fb4a27].
︙ | ︙ | |||
601 602 603 604 605 606 607 | } return ''; } static public function redirect(array $params): void { | > > > > > > > | | | > | 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 | } return ''; } static public function redirect(array $params): void { if (isset($params['force'])) { Utils::redirectDialog($params['force']); } elseif (isset($_GET['_dialog'])) { Utils::reloadParentFrame(); } else { Utils::redirectDialog($params['to'] ?? null); } } } |
Modified src/include/lib/Garradin/UserTemplate/Modules.php from [ff48e1b187] to [2092841626].
︙ | ︙ | |||
23 24 25 26 27 28 29 30 31 32 33 34 35 36 | class Modules { // Shortcuts so that code calling snippets method don't have to use Module entity const SNIPPET_TRANSACTION = Module::SNIPPET_TRANSACTION; const SNIPPET_USER = Module::SNIPPET_USER; const SNIPPET_HOME_BUTTON = Module::SNIPPET_HOME_BUTTON; static public function fetchDistFile(string $path): ?string { if (substr($path, 0, strlen('modules/')) === 'modules/') { $path = substr($path, strlen('modules/')); } | > > | 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | class Modules { // Shortcuts so that code calling snippets method don't have to use Module entity const SNIPPET_TRANSACTION = Module::SNIPPET_TRANSACTION; const SNIPPET_USER = Module::SNIPPET_USER; const SNIPPET_HOME_BUTTON = Module::SNIPPET_HOME_BUTTON; const SNIPPET_MY_SERVICES = Module::SNIPPET_MY_SERVICES; const SNIPPET_MY_DETAILS = Module::SNIPPET_MY_DETAILS; static public function fetchDistFile(string $path): ?string { if (substr($path, 0, strlen('modules/')) === 'modules/') { $path = substr($path, strlen('modules/')); } |
︙ | ︙ |
Modified src/include/lib/Garradin/UserTemplate/Sections.php from [414a4bc1b1] to [8a6c6f3cb2].
︙ | ︙ | |||
552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 | echo '</tbody>'; echo '</table>'; echo $list->getHTMLPagination(); } static public function balances(array $params, UserTemplate $tpl, int $line): \Generator { $db = DB::getInstance(); $params['where'] ??= ''; $params['tables'] = 'acc_accounts_balances'; if (isset($params['codes'])) { | > > > > > > > > > > > > > > > > > < < < < < < < < | < | | 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 | echo '</tbody>'; echo '</table>'; echo $list->getHTMLPagination(); } static protected function getAccountCodeCondition($codes, string $column = 'code') { if (!is_array($codes)) { $codes = explode(',', $codes); } $db = DB::getInstance(); foreach ($codes as &$code) { $code = $column . ' LIKE ' . $db->quote($code); } unset($code); return implode(' OR ', $codes); } static public function balances(array $params, UserTemplate $tpl, int $line): \Generator { $db = DB::getInstance(); $params['where'] ??= ''; $params['tables'] = 'acc_accounts_balances'; if (isset($params['codes'])) { $params['where'] .= sprintf(' AND (%s)', self::getAccountCodeCondition($params['codes'])); unset($params['codes']); } if (isset($params['year'])) { $params['where'] .= ' AND id_year = :year'; $params[':year'] = $params['year']; unset($params['year']); } |
︙ | ︙ | |||
654 655 656 657 658 659 660 661 662 663 664 665 666 667 | unset($params['id']); } elseif (isset($params['id'])) { $params['where'] .= ' AND users.id = :id'; $params[':id'] = (int) $params['id']; unset($params['id']); } if (!empty($params['search_name'])) { $params['tables'] .= sprintf(' INNER JOIN users_search AS us ON us.id = users.id AND %s LIKE :search_name ESCAPE \'\\\' COLLATE NOCASE', DynamicFields::getNameFieldsSQL('us')); $params[':search_name'] = '%' . Utils::unicodeTransliterate($params['search_name']) . '%'; unset($params['search_name']); } | > > > > > | 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 | unset($params['id']); } elseif (isset($params['id'])) { $params['where'] .= ' AND users.id = :id'; $params[':id'] = (int) $params['id']; unset($params['id']); } elseif (isset($params['id_parent'])) { $params['where'] .= ' AND users.id_parent = :id_parent'; $params[':id_parent'] = (int) $params['id_parent']; unset($params['id_parent']); } if (!empty($params['search_name'])) { $params['tables'] .= sprintf(' INNER JOIN users_search AS us ON us.id = users.id AND %s LIKE :search_name ESCAPE \'\\\' COLLATE NOCASE', DynamicFields::getNameFieldsSQL('us')); $params[':search_name'] = '%' . Utils::unicodeTransliterate($params['search_name']) . '%'; unset($params['search_name']); } |
︙ | ︙ | |||
720 721 722 723 724 725 726 | return self::sql($params, $tpl, $line); } static public function transactions(array $params, UserTemplate $tpl, int $line): \Generator { $params['where'] ??= ''; | < < < < < < > > > > > > > > > > > > > > > > > > > > > > > > > > | 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 | return self::sql($params, $tpl, $line); } static public function transactions(array $params, UserTemplate $tpl, int $line): \Generator { $params['where'] ??= ''; $id_field = DynamicFields::getNameFieldsSQL(); $params['select'] = sprintf('t.*, SUM(l.credit) AS credit, SUM(l.debit) AS debit, GROUP_CONCAT(DISTINCT a.code) AS accounts_codes, (SELECT GROUP_CONCAT(DISTINCT %s) FROM users WHERE id IN (SELECT id_user FROM acc_transactions_users WHERE id_transaction = t.id)) AS users_names', $id_field); $params['tables'] = 'acc_transactions AS t INNER JOIN acc_transactions_lines AS l ON l.id_transaction = t.id INNER JOIN acc_accounts AS a ON l.id_account = a.id'; $params['group'] = 't.id'; if (isset($params['id'])) { $params['where'] .= ' AND t.id = :id'; $params[':id'] = (int) $params['id']; unset($params['id']); } elseif (isset($params['user'])) { $params['where'] .= ' AND tu.id_user = :id_user'; $params[':id_user'] = (int) $params['user']; unset($params['user']); $params['tables'] .= ' INNER JOIN acc_transactions_users AS tu ON tu.id_transaction = t.id'; } if (isset($params['debit_codes'])) { $params['where'] .= sprintf(' AND l.debit > 0 AND (%s)', self::getAccountCodeCondition($params['debit_codes'], 'a.code')); } elseif (isset($params['credit_codes'])) { $params['where'] .= sprintf(' AND l.debit > 0 AND (%s)', self::getAccountCodeCondition($params['credit_codes'], 'a.code')); } unset($params['debit_codes'], $params['credit_codes']); if (isset($params['order']) && ctype_alpha(substr((string) $params['order'], 0, 1))) { $params['order'] = 't.' . $params['order']; } return self::sql($params, $tpl, $line); } static public function transaction_lines(array $params, UserTemplate $tpl, int $line): \Generator { $params['where'] ??= ''; |
︙ | ︙ |
Modified src/include/lib/Garradin/UserTemplate/UserTemplate.php from [9208421c53] to [f6edb30b14].
︙ | ︙ | |||
258 259 260 261 262 263 264 | try { $code = $this->compile($source); file_put_contents($tmp_path, $code); require $tmp_path; } catch (Brindille_Exception $e) { | | | 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | try { $code = $this->compile($source); file_put_contents($tmp_path, $code); require $tmp_path; } catch (Brindille_Exception $e) { $path = $this->file ? $this->file->path : ($this->code ? 'code' : str_replace(ROOT, '…', $this->path)); $message = sprintf("Erreur dans '%s' :\n%s", $path, $e->getMessage()); if (0 === strpos($this->path ?? '', self::DIST_ROOT)) { // We want errors in shipped code to be reported, it is not normal throw new \RuntimeException($message, 0, $e); } |
︙ | ︙ | |||
429 430 431 432 433 434 435 | try { return call_user_func($this->_functions[$name], $params, $this, $line); } catch (UserException $e) { throw $e; } catch (\Exception $e) { | | | 429 430 431 432 433 434 435 436 437 438 439 440 | try { return call_user_func($this->_functions[$name], $params, $this, $line); } catch (UserException $e) { throw $e; } catch (\Exception $e) { throw new Brindille_Exception(sprintf("line %d: function '%s' has returned an error: %s\nParameters: %s", $line, $name, $e->getMessage(), substr(var_export($params, true), 6)), 0, $e); } } } |
Modified src/include/lib/Garradin/Users/Categories.php from [45faa8b425] to [2d9990cfea].
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?php namespace Garradin\Users; use Garradin\DB; use Garradin\Entities\Users\Category; use KD2\DB\EntityManager as EM; class Categories { const HIDDEN_ONLY = 1; const WITHOUT_HIDDEN = 0; | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?php namespace Garradin\Users; use Garradin\DB; use Garradin\Entities\Users\Category; use Garradin\Entities\Users\User; use KD2\DB\EntityManager as EM; class Categories { const HIDDEN_ONLY = 1; const WITHOUT_HIDDEN = 0; |
︙ | ︙ | |||
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | static public function listAssoc(?int $hidden = null): array { return DB::getInstance()->getAssoc(sprintf('SELECT id, name FROM %s WHERE 1 %s ORDER BY name COLLATE U_NOCASE;', Category::TABLE, self::getHiddenClause($hidden) )); } static public function listWithStats(?int $hidden = null): array { return DB::getInstance()->getGrouped(sprintf('SELECT c.id, c.*, (SELECT COUNT(*) FROM users WHERE id_category = c.id) AS count FROM %s c WHERE 1 %s ORDER BY c.name COLLATE U_NOCASE;', Category::TABLE, self::getHiddenClause($hidden) )); } } | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | static public function listAssoc(?int $hidden = null): array { return DB::getInstance()->getAssoc(sprintf('SELECT id, name FROM %s WHERE 1 %s ORDER BY name COLLATE U_NOCASE;', Category::TABLE, self::getHiddenClause($hidden) )); } static public function listAssocWithStats(?int $hidden = null): array { $db = DB::getInstance(); $categories = [0 => (object) [ 'label' => 'Toutes, sauf cachées', 'count' => $db->count(User::TABLE, 'id_category NOT IN (SELECT id FROM users_categories WHERE hidden = 1)'), ]]; if ($hidden !== self::WITHOUT_HIDDEN) { $categories[-1] = (object) [ 'label' => 'Toutes, même cachées', 'count' => $db->count(User::TABLE), ]; } $format = '%s (%d membres)'; return $categories + $db->getGrouped(sprintf( 'SELECT id, name AS label, (SELECT COUNT(*) FROM %s WHERE %1$s.id_category = %s.id) AS count FROM %2$s WHERE 1 %s ORDER BY name COLLATE U_NOCASE;', User::TABLE, Category::TABLE, self::getHiddenClause($hidden) )); } static public function listWithStats(?int $hidden = null): array { return DB::getInstance()->getGrouped(sprintf('SELECT c.id, c.*, (SELECT COUNT(*) FROM users WHERE id_category = c.id) AS count FROM %s c WHERE 1 %s ORDER BY c.name COLLATE U_NOCASE;', Category::TABLE, self::getHiddenClause($hidden) )); } } |
Modified src/include/lib/Garradin/Users/DynamicFields.php from [1a3960695b] to [b9ee91a2f6].
︙ | ︙ | |||
597 598 599 600 601 602 603 | return $c = array_combine($c, $c); } public function getSQLCopy(string $old_table_name, string $new_table_name = User::TABLE, array $fields = null, string $function = null): string { $db = DB::getInstance(); unset($fields['id']); | > | > > > > > > > > > > > | 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 | return $c = array_combine($c, $c); } public function getSQLCopy(string $old_table_name, string $new_table_name = User::TABLE, array $fields = null, string $function = null): string { $db = DB::getInstance(); unset($fields['id']); $source = []; foreach ($fields as $src_key => $dst_key) { $field = $this->get($dst_key); if ($field) { $source[] = sprintf('CAST(%s AS %s)', $db->quoteIdentifier($src_key), $field->sql_type()); } else { $source[] = $src_key; } } if ($function) { $source = array_map(fn($a) => $function . '(' . $a . ')', $source); } return sprintf('INSERT INTO %s (id, %s) SELECT id, %s FROM %s;', $new_table_name, |
︙ | ︙ | |||
946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 | $db = DB::getInstance(); // First check that the field can be used as login if (!$this->isUnique($new_field)) { throw new UserException(sprintf('Le champ "%s" comporte des doublons et ne peut donc pas servir comme identifiant unique de connexion.', $this->_fields[$new_field]->label)); } $sql = sprintf('UPDATE %s SET system = system & ~%d WHERE system & %2$d; UPDATE %1$s SET system = system | %2$d WHERE name = %s;', self::TABLE, DynamicField::LOGIN, $db->quote($new_field) ); $db->exec($sql); // Regenerate login index $db->exec('DROP INDEX IF EXISTS users_id_field;'); $this->createIndexes(); | > > > > < < | 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 | $db = DB::getInstance(); // First check that the field can be used as login if (!$this->isUnique($new_field)) { throw new UserException(sprintf('Le champ "%s" comporte des doublons et ne peut donc pas servir comme identifiant unique de connexion.', $this->_fields[$new_field]->label)); } // Change login field in fields config table $sql = sprintf('UPDATE %s SET system = system & ~%d WHERE system & %2$d; UPDATE %1$s SET system = system | %2$d WHERE name = %s;', self::TABLE, DynamicField::LOGIN, $db->quote($new_field) ); $db->exec($sql); // Reload dynamic fields cache $this->reload(); // Regenerate login index $db->exec('DROP INDEX IF EXISTS users_id_field;'); $this->createIndexes(); } public function listEligibleNameFields(): array { $out = []; foreach ($this->_fields as $field) { |
︙ | ︙ |
Modified src/include/lib/Garradin/Users/Users.php from [4ae5046163] to [48d8e7ac83].
︙ | ︙ | |||
139 140 141 142 143 144 145 | { $df = DynamicFields::getInstance(); $number_field = $df->getNumberField(); $name_fields = $df->getNameFields(); $columns = [ '_user_id' => [ | | | | > | 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | { $df = DynamicFields::getInstance(); $number_field = $df->getNumberField(); $name_fields = $df->getNameFields(); $columns = [ '_user_id' => [ 'select' => 'users.id', ], ]; $number_column = [ 'label' => 'Num.', 'select' => 'users.' . $number_field, ]; $identity_column = [ 'label' => $df->getNameLabel(), 'select' => $df->getNameFieldsSQL('users'), 'order' => 'identity COLLATE U_NOCASE %s', ]; $fields = $df->getListedFields(); foreach ($fields as $key => $config) { // Skip number field if ($key === $number_field) { |
︙ | ︙ | |||
176 177 178 179 180 181 182 | $identity_column = null; } continue; } $columns[$key] = [ | | > > > > > > > > > > > > > > > > > > > > > > > > > > | | | 177 178 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 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 | $identity_column = null; } continue; } $columns[$key] = [ 'label' => $config->label, 'select' => 'users.' . $key, ]; if ($df->isText($key)) { $columns[$key]['order'] = sprintf('%s COLLATE U_NOCASE %%s', $key); } } if (null !== $identity_column) { $columns['identity'] = $identity_column; } $tables = User::TABLE; $db = DB::getInstance(); if ($db->test('users', 'is_parent = 1')) { $tables .= ' LEFT JOIN users b ON b.id = users.id_parent'; $columns['id_parent'] = [ 'label' => 'Rattaché à', 'select' => 'users.id_parent', 'order' => 'users.id_parent IS NULL, _parent_name COLLATE U_NOCASE %s, identity COLLATE U_NOCASE %1$s', ]; $columns['_parent_name'] = [ 'select' => sprintf('CASE WHEN users.id_parent IS NOT NULL THEN %s ELSE NULL END', $df->getNameFieldsSQL('b')), ]; $columns['is_parent'] = [ 'label' => 'Responsable', 'select' => 'users.is_parent', 'order' => 'users.is_parent DESC, identity COLLATE U_NOCASE %1$s', ]; } if (!$id_category) { $conditions = sprintf('users.id_category IN (SELECT id FROM users_categories WHERE hidden = 0)'); } elseif ($id_category > 0) { $conditions = sprintf('users.id_category = %d', $id_category); } else { $conditions = '1'; } $order = 'identity'; |
︙ | ︙ |
Modified src/include/lib/Garradin/Web/Render/AbstractRender.php from [b9ae79bdbd] to [a060bb9bc7].
1 2 3 4 5 6 7 8 9 10 11 12 | <?php namespace Garradin\Web\Render; use Garradin\Entities\Files\File; use Garradin\Utils; use const Garradin\{WWW_URL, ADMIN_URL}; abstract class AbstractRender { protected $current_path; | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 | <?php namespace Garradin\Web\Render; use Garradin\Entities\Files\File; use Garradin\Files\Files; use Garradin\Utils; use const Garradin\{WWW_URL, ADMIN_URL}; abstract class AbstractRender { protected $current_path; |
︙ | ︙ | |||
35 36 37 38 39 40 41 42 43 44 45 46 47 48 | return isset($this->current_path); } public function registerAttachment(string $uri) { Render::registerAttachment($this->file, $uri); } public function resolveAttachment(string $uri) { $prefix = $this->current_path; $pos = strpos($uri, '/'); if ($pos === 0) { | > > > > > > > > > > > > > > > > > > > > | 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 | return isset($this->current_path); } public function registerAttachment(string $uri) { Render::registerAttachment($this->file, $uri); } public function listImages(): array { if (!$this->file) { return []; } $out = []; $list = Files::list(Utils::dirname($this->file->path)); foreach ($list as $file) { if (!$file->image) { continue; } $out[] = $file->name; } return $out; } public function resolveAttachment(string $uri) { $prefix = $this->current_path; $pos = strpos($uri, '/'); if ($pos === 0) { |
︙ | ︙ |
Modified src/include/lib/Garradin/Web/Render/Extensions.php from [79ca1d8f76] to [5f09fceffa].
︙ | ︙ | |||
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | public function getList(): array { $list = [ 'file' => [$this, 'file'], 'fichier' => [$this, 'file'], 'image' => [$this, 'image'], ]; Plugins::fireSignal('render.extensions.init', ['extensions' => &$list]); return $list; } public function file(bool $block, array $args): string { $name = $args[0] ?? null; $caption = $args[1] ?? null; if (!$name || !$this->renderer->hasPath()) { | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | public function getList(): array { $list = [ 'file' => [$this, 'file'], 'fichier' => [$this, 'file'], 'image' => [$this, 'image'], 'gallery' => [$this, 'gallery'], 'video' => [$this, 'video'], ]; Plugins::fireSignal('render.extensions.init', ['extensions' => &$list]); return $list; } public function gallery(bool $block, array $args, ?string $content): string { $type = 'gallery'; if (isset($args['type'])) { $type = $args['type']; } elseif (isset($args[0])) { $type = $args[0]; } if (!in_array($type, ['gallery', 'slideshow'])) { $type = 'gallery'; } $out = sprintf('<div class="%s"><div class="images">', $type); $index = ''; if (trim((string)$content) === '') { $images = $this->renderer->listImages(); } else { $images = explode("\n", $content); } $i = 1; foreach ($images as $line) { $line = trim($line); if ($line === '') { continue; } $img = strtok($line, '|'); $label = strtok(false); $size = $type == 'slideshow' ? 500 : 200; $out .= sprintf('<figure>%s</figure>', $this->img($img, $size, $label ?: null)); } $out .= '</div></div>'; return $out; } public function file(bool $block, array $args): string { $name = $args[0] ?? null; $caption = $args[1] ?? null; if (!$name || !$this->renderer->hasPath()) { |
︙ | ︙ | |||
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | $ext = substr($name, strrpos($name, '.')+1); return sprintf( '<aside class="file" data-type="%s"><a href="%s" class="internal-file"><b>%s</b> <small>(%s)</small></a></aside>', htmlspecialchars($ext), htmlspecialchars($url), htmlspecialchars($caption), htmlspecialchars(strtoupper($ext)) ); } public function image(bool $block, array $args): string { static $align_replace = ['gauche' => 'left', 'droite' => 'right', 'centre' => 'center']; $name = $args['file'] ?? ($args[0] ?? null); $align = $args['align'] ?? ($args[1] ?? null); $caption = $args['caption'] ?? (isset($args[2]) ? implode(' ', array_slice($args, 2)) : null); $align = strtr((string)$align, $align_replace); if (!$name || !$this->renderer->hasPath()) { return self::error('Tag image : aucun nom de fichier indiqué.'); } | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > < < < | < < < < < < < < < | > > > > > > > > > > > > > > > > > | 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 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | $ext = substr($name, strrpos($name, '.')+1); return sprintf( '<aside class="file" data-type="%s"><a href="%s" class="internal-file"><b>%s</b> <small>(%s)</small></a></aside>', htmlspecialchars($ext), htmlspecialchars($url), htmlspecialchars($caption), htmlspecialchars(strtoupper($ext)) ); } public function video(bool $block, array $args): string { $name = $args['file'] ?? ($args[0] ?? null); if (!$name || !$this->renderer->hasPath()) { return self::error('Tag image : aucun nom de fichier indiqué.'); } $poster = $args['poster'] ?? ($args[1] ?? null); $subs = $args['subtitles'] ?? ($args[2] ?? null); $url = $this->renderer->resolveAttachment($name); if ($poster) { $poster = $this->renderer->resolveAttachment($poster); } if ($subs) { $subs = $this->renderer->resolveAttachment($subs); $subs = sprintf('<track kind="subtitles" default="true" src="%s" />', htmlspecialchars($subs)); } $params = ''; if (isset($args['width'])) { $params .= sprintf(' width="%d"', $args['width']); } if (isset($args['height'])) { $params .= sprintf(' height="%d"', $args['height']); } return sprintf('<video controls="true" preload="%s" poster="%s" src="%s"%s>%s</video>', $poster ? 'metadata' : 'none', htmlspecialchars($poster), htmlspecialchars($url), $params, $subs ); } public function image(bool $block, array $args): string { static $align_replace = ['gauche' => 'left', 'droite' => 'right', 'centre' => 'center']; $name = $args['file'] ?? ($args[0] ?? null); $align = $args['align'] ?? ($args[1] ?? null); $caption = $args['caption'] ?? (isset($args[2]) ? implode(' ', array_slice($args, 2)) : null); $align = strtr((string)$align, $align_replace); if (!$name || !$this->renderer->hasPath()) { return self::error('Tag image : aucun nom de fichier indiqué.'); } $size = $align == 'center' ? 500 : 200; $out = $this->img($name, $size, $caption); if (!empty($align)) { if ($caption) { $caption = sprintf('<figcaption>%s</figcaption>', htmlspecialchars($caption)); } $out = sprintf('<figure class="image img-%s">%s%s</figure>', $align, $out, $caption); } return $out; } protected function img(string $name, ?int $thumb_size = 200, ?string $caption = null): string { $url = $this->renderer->resolveAttachment($name); $svg = substr($name, -4) == '.svg'; $thumb_url = null; if (!$svg) { $thumb_url = sprintf('%s?%spx', $url, $thumb_size); } return sprintf('<a href="%s" class="internal-image" target="_image"><img src="%s" alt="%s" loading="lazy" /></a>', htmlspecialchars($url), htmlspecialchars($thumb_url ?? $url), htmlspecialchars($caption ?? '') ); } } |
Modified src/include/lib/Garradin/Web/Render/Markdown.php from [9b15706bc5] to [7b5fba1897].
︙ | ︙ | |||
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | /** * Used by doc_md_to_html.php script */ public $toc = []; public function render(?string $content = null): string { $md = Markdown_Parser::instance(); Markdown_Extensions::register($md); // Register Paheko extensions $ext = new Extensions($this); foreach ($ext->getList() as $name => $callback) { $md->registerExtension($name, $callback); } | > > > > > > > > < | | | | | 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 | /** * Used by doc_md_to_html.php script */ public $toc = []; public function render(?string $content = null): string { if (null === $content && $this->file) { $content = $this->file->fetch(); } if (empty($content)) { return $content; } $md = Markdown_Parser::instance(); Markdown_Extensions::register($md); // Register Paheko extensions $ext = new Extensions($this); foreach ($ext->getList() as $name => $callback) { $md->registerExtension($name, $callback); } $content = $md->text($content); unset($md); $content = preg_replace_callback(';<a href="([\w_-]+?)">;i', function ($matches) { return sprintf('<a href="%s">', htmlspecialchars($this->resolveLink(htmlspecialchars_decode($matches[1])))); }, $content); return sprintf('<div class="web-content">%s</div>', $content); } } |
Modified src/include/lib/Garradin/Web/Router.php from [e588ab7eaa] to [3c49b52d72].
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 24 25 | use Garradin\Config; use Garradin\Plugins; use Garradin\UserException; use Garradin\Utils; use Garradin\UserTemplate\Modules; use Garradin\Users\Session; use const Garradin\{WWW_URI, ADMIN_URL, ROOT, HTTP_LOG_FILE, ENABLE_XSENDFILE}; class Router { const DAV_ROUTES = [ 'dav', | > > | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | use Garradin\Config; use Garradin\Plugins; use Garradin\UserException; use Garradin\Utils; use Garradin\UserTemplate\Modules; use Garradin\Users\Session; use \KD2\HTML\Markdown; use const Garradin\{WWW_URI, ADMIN_URL, ROOT, HTTP_LOG_FILE, ENABLE_XSENDFILE}; class Router { const DAV_ROUTES = [ 'dav', |
︙ | ︙ | |||
128 129 130 131 132 133 134 135 136 137 138 139 140 141 | Plugins::fireSignal('http.request.file.after', compact('file', 'uri', 'session')); return; } Modules::route($uri); } static public function log(string $message, ...$params) { if (!HTTP_LOG_FILE) { return; } | > > > > > > > > > > > > > > > > > > > > | 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 160 161 162 163 | Plugins::fireSignal('http.request.file.after', compact('file', 'uri', 'session')); return; } Modules::route($uri); } static public function markdown(string $text) { $md = new Markdown; header('Content-Type: text/html'); $text = $md->text($text); $title = ''; if (preg_match('!<h1[^>]*>(.*?)</h1>!is', $text, $match)) { $title = strip_tags($match[1]); } printf('<!DOCYPE html><head><title>%s</title> <style type="text/css">body { font-family: Verdana, sans-serif; padding: .5em; margin: 0; background: #fff; color: #000; }</style> <link rel="stylesheet" type="text/css" href="%scss.php" /></head><body>', $title, ADMIN_URL); echo $text; } static public function log(string $message, ...$params) { if (!HTTP_LOG_FILE) { return; } |
︙ | ︙ |
Modified src/modules/carte_membre/_carte.html from [4ddefd6010] to [57aa1ec8a2].
1 2 3 4 5 6 7 8 9 10 11 12 | {{if $module.config.photo}} {{:assign var="_photo" from="%s.0"|args:$module.config.photo}} {{/if}} <article{{if $module.config.logo == 1 || $_photo}} class="with-images"{{/if}}> {{if $module.config.logo == 1 && $config.files.logo}} <img src="{{$config.files.logo}}?150px" alt="" class="logo" /> {{elseif $module.config.logo == 2 && $config.files.logo}} <img src="{{$config.files.logo}}?500px" alt="" class="bglogo" /> {{/if}} {{if $_photo}} | | | > > > > > > > > > | > > | 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 | {{if $module.config.photo}} {{:assign var="_photo" from="%s.0"|args:$module.config.photo}} {{/if}} <article{{if $module.config.logo == 1 || $_photo}} class="with-images"{{/if}}> {{if $module.config.logo == 1 && $config.files.logo}} <img src="{{$config.files.logo}}?150px" alt="" class="logo" /> {{elseif $module.config.logo == 2 && $config.files.logo}} <img src="{{$config.files.logo}}?500px" alt="" class="bglogo" /> {{/if}} {{if $_photo}} <img src="{{$_photo.url}}?crop-256px" alt="" class="photo" /> {{/if}} <div> {{if $module.config.fields|has:$number_field}} <div class="number{{if $_photo}} with-photo{{/if}}{{if $module.config.logo == 1}} with-logo{{/if}}"><span>N°</span><span>{{$_number}}</span></div> {{/if}} {{if $module.config.header}} <div class="content"> {{$module.config.header|markdown|raw}} </div> {{/if}} <h1>{{$_name}}</h1> {{if $module.config.fields}} {{#select name, label FROM config_users_fields}} {{:assign var="fields.%s"|args:$name value=$label}} {{/select}} <ul class="fields"> {{if $module.config.fields|has:"_category"}} <li> {{if $module.config.show_fields_names}}Catégorie :{{/if}} {{#select name FROM users_categories WHERE id = {$id_category|intval};}} {{$name}} {{/select}} </li> {{/if}} {{#foreach from=$module.config.fields item="key"}} {{:assign var="value" from=$key}} {{if $value && $key != $number_field}} <li> {{if $module.config.show_fields_names}}{{:assign var="label" from="fields.%s"|args:$key}}{{$label}} :{{/if}} {{$value}} </li> {{/if}} {{/foreach}} </ul> {{/if}} {{if $module.config.id_service}} {{#subscriptions user=$id id_service=$module.config.id_service active=true}} |
︙ | ︙ |
Modified src/modules/carte_membre/carte.css from [ab0902fbf3] to [76742380e2].
︙ | ︙ | |||
14 15 16 17 18 19 20 | main { display: flex; flex-wrap: wrap; grid-gap: 10mm; align-items: center; justify-content: center; | < | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | main { display: flex; flex-wrap: wrap; grid-gap: 10mm; align-items: center; justify-content: center; } main.preview { background: #fff; padding: 2em; } |
︙ | ︙ | |||
64 65 66 67 68 69 70 | } ul { list-style: none; } .logo { max-width: 100px; | | | | 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | } ul { list-style: none; } .logo { max-width: 100px; max-height: 100px; position: absolute; right: 0; bottom: 0; } .photo { max-width: 100px; max-height: 100px; position: absolute; right: 0; top: 0; z-index: 100; } .bglogo { position: absolute; |
︙ | ︙ | |||
98 99 100 101 102 103 104 | } h1 { font-size: 14pt; margin-bottom: .2em; } .number { position: absolute; | | | < > > > > > > > > | 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 | } h1 { font-size: 14pt; margin-bottom: .2em; } .number { position: absolute; right: 5px; top: 5px; border-radius: 50%; height: 15mm; width: 15mm; background: #666; color: #fff; display: flex; align-items: center; flex-direction: column; justify-content: center; border: 3px solid #fff; z-index: 10000; } .number.with-photo { bottom: 5px; top: auto; } .number.with-photo.with-logo { right: calc(100px / 2); top: calc(55mm / 2 - 15mm / 2); } .number span:nth-child(1) { font-size: 8pt; margin-top: -.7em; } .number span:nth-child(2) { font-size: 14pt; } h3 { margin-top: .5em; font-weight: normal; font-size: 11pt; } |
Modified src/modules/carte_membre/config.html from [66c0fe91c5] to [2c4270c273].
1 2 | {{:admin_header title="Configuration des cartes de membres"}} | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | {{:admin_header title="Configuration des cartes de membres"}} {{#form on="save"}} {{:save key="config" validate_schema="./config.schema.json" header=$_POST.header|trim|or:null logo=$_POST.logo|intval fields=$_POST.fields|or:null photo=$_POST.photo|or:null id_service=$_POST.id_service|intval|or:null show_fields_names=$_POST.show_fields_names|boolval }} {{:redirect to="?ok=1"}} {{/form}} {{if $_GET.ok}} <p class="block confirm">Configuration enregistrée.</p> {{/if}} <form method="post" action=""> |
︙ | ︙ | |||
30 31 32 33 34 35 36 37 38 39 40 41 42 43 | <dt><label for="f_logo_0">Affichage du logo de l'association</label></dt> {{:input type="radio" name="logo" value=0 source=$module.config label="Ne pas afficher" default=1}} {{:input type="radio" name="logo" value=1 source=$module.config label="En petit en bas à droite" default=1}} {{:input type="radio" name="logo" value=2 source=$module.config label="En filigrane (fond)"}} <dt>Champs des fiches membre à afficher sur la carte de membre</dt> <dd class="help"><em>(Le nom du membre est toujours affiché.)</em></dd> {{#select * FROM config_users_fields WHERE type NOT IN ('file', 'password', 'generated', 'multiple', 'checkbox', 'date', 'datetime') AND system & (1 << 4) = 0 ORDER BY sort_order}} {{if $module.config.fields|has:$name}} {{:assign var="checked" value=$name}} {{else}} {{:assign var="checked" value=null}} {{/if}} | > > > > > > > | 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | <dt><label for="f_logo_0">Affichage du logo de l'association</label></dt> {{:input type="radio" name="logo" value=0 source=$module.config label="Ne pas afficher" default=1}} {{:input type="radio" name="logo" value=1 source=$module.config label="En petit en bas à droite" default=1}} {{:input type="radio" name="logo" value=2 source=$module.config label="En filigrane (fond)"}} <dt>Champs des fiches membre à afficher sur la carte de membre</dt> <dd class="help"><em>(Le nom du membre est toujours affiché.)</em></dd> {{if $module.config.fields|has:"_category"}} {{:assign var="checked" value="_category"}} {{else}} {{:assign var="checked" value=null}} {{/if}} {{:input type="checkbox" name="fields[]" value="_category" label="Catégorie" default=$checked}} {{#select * FROM config_users_fields WHERE type NOT IN ('file', 'password', 'generated', 'multiple', 'checkbox', 'date', 'datetime') AND system & (1 << 4) = 0 ORDER BY sort_order}} {{if $module.config.fields|has:$name}} {{:assign var="checked" value=$name}} {{else}} {{:assign var="checked" value=null}} {{/if}} |
︙ | ︙ |
Added src/modules/carte_membre/snippets/my_details.html version [c8b74bdcfe].
> | 1 | {{:include file="./user_details.html"}} |
Modified src/modules/carte_membre/snippets/user_details.html from [63b08612f1] to [0ba9219156].
1 2 3 4 5 6 7 | <h2 class="ruler">{{$module.label}}</h2> <div style="display: flex; justify-content: center; gap: 30px"> <div style="width: 85mm; height: 55mm; box-shadow: 2px 2px 10px #000; border: 1px solid #000; overflow: hidden;" > <iframe src="{{"%scarte.html?id=%d&mode=embed"|args:$module.url:$user.id}}" scrolling="no" frameborder="0" width="100%" height="100%"></iframe> </div> <p style="display: flex; flex-direction: column;"> | < | 1 2 3 4 5 6 7 8 9 10 11 | <h2 class="ruler">{{$module.label}}</h2> <div style="display: flex; justify-content: center; gap: 30px"> <div style="width: 85mm; height: 55mm; box-shadow: 2px 2px 10px #000; border: 1px solid #000; overflow: hidden;" > <iframe src="{{"%scarte.html?id=%d&mode=embed"|args:$module.url:$user.id}}" scrolling="no" frameborder="0" width="100%" height="100%"></iframe> </div> <p style="display: flex; flex-direction: column;"> {{:linkbutton href="%scarte.html?id=%d&print=pdf&mode=print"|args:$module.url:$user.id label="Télécharger en PDF" shape="download"}} {{:linkbutton href="%scarte.html?id=%d&print=yes&mode=print"|args:$module.url:$user.id target="_blank" label="Imprimer" shape="print"}} </p> </div> |
Modified src/modules/ouvertures/config.js from [19286da6a6] to [1cf866bf6c].
1 2 3 4 | if (!open_data) { open_data = { 'closed': [{'close_day': '25', 'close_month': 'december', 'reopen_day': '2', 'reopen_month': 'january'}], 'open': [{ | | | 1 2 3 4 5 6 7 8 9 10 11 12 | if (!open_data) { open_data = { 'closed': [{'close_day': '25', 'close_month': 'december', 'reopen_day': '2', 'reopen_month': 'january'}], 'open': [{ 'frequency': 'this', 'day': 'saturday', 'open': '15:00', 'close': '19:00' }] }; } |
︙ | ︙ |
Name change from src/modules/receipt/recu_paiement/icon.svg to src/modules/receipt/icon.svg.
Modified src/modules/receipt/index.html from [98428c1160] to [0558d5863a].
|
| > > > > > > > > > > > | | > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | {{if $_GET.me}} {{#transactions user=$logged_user.id}} {{if $id == $_GET.me}} {{:assign id=$id}} {{:break}} {{/if}} {{/transactions}} {{if !$id}} {{:error message="Ce reçu n'existe pas"}} {{/if}} {{else}} {{#restrict block=true section="accounting" level="read"}} {{/restrict}} {{:assign id=$_GET.id}} {{/if}} {{if !$id}} {{:admin_header title="Reçu de paiement" current="acc"}} <p class="error block">Aucun numéro d'écriture n'a été fourni.</p> <form method="get" action=""> <fieldset> |
︙ | ︙ | |||
18 19 20 21 22 23 24 | {{:button type="submit" name="save" label="Voir le reçu (PDF)" shape="right" class="main"}} </p> </form> {{:admin_footer}} {{else}} | | | 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | {{:button type="submit" name="save" label="Voir le reçu (PDF)" shape="right" class="main"}} </p> </form> {{:admin_footer}} {{else}} {{#transactions id=$id}} {{:include file="./_header.html" page_size="A5" title="Reçu de paiement %d - %s"|args:$id:$users_names}} <h1>Reçu de paiement</h1> <h4>Référence n°{{$id}} — {{$date|date_short}}</h4> <p>L'association « {{$config.nom_asso}} » atteste avoir reçu de la part de :</p> |
︙ | ︙ |
Modified src/modules/receipt/module.ini from [718766f439] to [3dc086e733].
1 | name="Reçu de paiement" | | | 1 2 3 4 5 | name="Reçu de paiement" description="Reçu de paiement simple. Le reçu sera accessible sous chaque écriture comptable, et dans la page 'Mes activités' de chaque membre." author="Paheko" author_url="https://paheko.cloud/" system=1 |
Deleted src/modules/receipt/recu_paiement/index.html version [d1084f817e].
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted src/modules/receipt/recu_paiement/module.ini version [9fd092515c].
|
| < < < < |
Deleted src/modules/receipt/recu_paiement/snippets/transaction_details.html version [669dfa2423].
|
| < < < < < < < < |
Added src/modules/receipt/snippets/my_services.html version [4a0e099d84].
> > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 | <h2 class="ruler">Reçus de paiements</h2> <table class="list"> {{#transactions user=$logged_user.id order="date DESC" debit_codes="5%"}} <tr> <th>{{$date|date_short}}</th> <td>{{$label}}</td> <td>{{$debit|money_currency}}</td> <td class="actions">{{:linkbutton href="%s?me=%d&print=pdf"|args:$module.url:$id shape="download" label="Télécharger le reçu (PDF)"}}</td> </tr> {{/transactions}} </table> |
Modified src/modules/recus_fiscaux/nouveau.html from [8bd4466c59] to [554af72263].
︙ | ︙ | |||
317 318 319 320 321 322 323 | let p = $('[name=preview]')[0]; p.addEventListener('click', (e) => { let form = e.target.form; form.action = "previsualiser.html"; form.target = "dialog"; | | | 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 | let p = $('[name=preview]')[0]; p.addEventListener('click', (e) => { let form = e.target.form; form.action = "previsualiser.html"; form.target = "dialog"; g.openFrameDialog('about:blank', {height: 'auto'}); form.submit(); form.action = ""; form.target = ""; }); </script> {{/if}} {{:admin_footer}} |
Modified src/modules/remise_cheques/snippets/transaction_details.html from [103614231d] to [12502e2b48].
1 | {{if $module.config.accounts === null}} | | | 1 2 3 4 5 6 7 | {{if $module.config.accounts === null}} {{:assign var="module.config.accounts" value='5112'}} {{/if}} {{* FIXME: ne proposer le bordereau que pour les écritures de dépot *}} {{:include file="/receipt/snippets/transaction_details.html"}} |
Modified src/modules/web/content.css from [5103742927] to [6a0e1ef717].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | /** * Ce fichier contient les styles CSS qui s'appliquent au contenu des articles et catégorie, * que ce soit sur le site public ou dans la prévisualisation de l'administration. * * Généralement il n'est pas nécessaire de le modifier. */ .protected-contact::before { content: attr(data-a) "\0040" attr(data-b) "." attr(data-c); } .web-content p, .web-content h1, .web-content h2, .web-content h3, .web-content h4, .web-content h5, .web-content h6, .web-content ul, .web-content ol, .web-content table, .web-content blockquote, .web-content pre { margin-bottom: 1rem; } .web-content ul, .web-content ol, .web-content dd { | > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | /** * Ce fichier contient les styles CSS qui s'appliquent au contenu des articles et catégorie, * que ce soit sur le site public ou dans la prévisualisation de l'administration. * * Généralement il n'est pas nécessaire de le modifier. */ .protected-contact::before { content: attr(data-a) "\0040" attr(data-b) "." attr(data-c); } .web-content figure { margin: 0; padding: 0; } .web-content p, .web-content h1, .web-content h2, .web-content h3, .web-content h4, .web-content h5, .web-content h6, .web-content ul, .web-content ol, .web-content table, .web-content blockquote, .web-content pre { margin-bottom: 1rem; } .web-content ul, .web-content ol, .web-content dd { |
︙ | ︙ | |||
251 252 253 254 255 256 257 258 259 260 261 262 263 264 | right: 0; bottom: 0; } .web-content figure.video a:hover img { opacity: 0.7; } .web-content figure.image figcaption { font-style: italic; color: #666; margin-top: 2pt; } | > > > > > | 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 | right: 0; bottom: 0; } .web-content figure.video a:hover img { opacity: 0.7; } .web-content video { margin: .8em auto; display: block; } .web-content figure.image figcaption { font-style: italic; color: #666; margin-top: 2pt; } |
︙ | ︙ | |||
288 289 290 291 292 293 294 295 296 297 298 299 300 301 | .web-content a.internal-image { cursor: zoom-in; } .web-content img, .web-content object { max-width: 100%; } .imageBrowser { position: fixed; top: 0; left: 0; right: 0; bottom: 0; | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 298 299 300 301 302 303 304 305 306 307 308 309 310 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 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 | .web-content a.internal-image { cursor: zoom-in; } .web-content img, .web-content object { max-width: 100%; } .web-content .gallery, .web-content .slideshow { margin: 1em; } .web-content .gallery .images { display: flex; flex-wrap: wrap; grid-gap: 10px; justify-content: center; } .web-content .gallery figure { flex: 1 1 auto; max-height: 180px; } .web-content .gallery img { object-fit: cover; width: 100%; height: 100%; vertical-align: middle; } .web-content .gallery .images::after { content: ""; flex-grow: 1; width: 1px; display: block; } .web-content .slideshow { position: relative; width: calc(500px + 5em); margin: 1em auto; } .web-content .slideshow .images { display: block; position: relative; overflow: hidden; margin: 0 auto; height: 400px; width: 500px; white-space: nowrap; } .web-content .slideshow .index { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; } .web-content .slideshow button { border: none; border-radius: 50%; background: rgba(0, 0, 0, .2); color: #000; font-weight: bold; font-size: 12pt; min-width: 3ch; height: 3ch; margin: .2em; cursor: pointer; text-align: center; } .web-content .slideshow button:hover { color: darkred; background: rgba(255, 255, 255, .2); box-shadow: 0px 0px 5px #000; } .web-content .slideshow button.current { background: rgba(255, 165, 0, .3); box-shadow: 0px 0px 5px #999; } .web-content .slideshow .nav { position: absolute; top: 0; left: 0; right: 0; height: 400px; display: flex; align-items: center; justify-content: space-between; } .web-content .slideshow figure, .web-content .slideshow a { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; } .web-content .slideshow img { max-width: 100%; max-height: 95%; } .imageBrowser { position: fixed; top: 0; left: 0; right: 0; bottom: 0; |
︙ | ︙ |
Modified src/templates/_foot.tpl from [9203feaf66] to [fd3f94279f].
1 2 3 | </main> <script type="text/javascript" defer="defer"> | > > > | < < | | | | | > | | < < > > | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | </main> {if $is_logged} {* Keep session alive by requesting renewal every before it expires *} <script type="text/javascript" defer="defer"> (function () {ldelim} var keep_session_url = "{$admin_url}login.php?keepSessionAlive&"; var session_gc = <?=intval(ini_get('session.gc_maxlifetime'))?>; window.setInterval( () => fetch(g.admin_url + 'login.php?keepSessionAlive&' + (+new Date)), (session_gc - 5*60)*1000 ); {if !LOCAL_LOGIN && $config.auto_logout && !$session->hasRememberMeCookie()} g.auto_logout = {$config.auto_logout}; g.script('scripts/auto_logout.js'); {/if} {rdelim})(); </script> {/if} </body> </html> |
Modified src/templates/acc/projects/_list.tpl from [e8e8a942d5] to [9793637ed8].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <table class="list projects"> {if !empty($caption)}<caption>{$caption}</caption>{/if} <thead> <tr> <td>Projet</td> <td></td> <td class="money">Charges</td> <td class="money">Produits</td> <td class="money">Débits</td> <td class="money">Crédits</td> <td class="money">Solde</td> </tr> </thead> {foreach from=$list item="parent"} <tbody{if $parent.archived} class="archived"{/if}> | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <table class="list projects"> {if !empty($caption)}<caption>{$caption}</caption>{/if} <thead> <tr> <td>Projet</td> <td></td> <td class="money">Charges</td> <td class="money">Produits</td> <td class="money">Résultat</td> <td class="money">Débits</td> <td class="money">Crédits</td> <td class="money">Solde</td> </tr> </thead> {foreach from=$list item="parent"} <tbody{if $parent.archived} class="archived"{/if}> |
︙ | ︙ | |||
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 | <p class="actions"> {linkbutton href="!acc/reports/ledger.php?projects_only=1&year=%d"|args:$parent.id_year label="Grand livre analytique"} </p> {/if} </th> </tr> {foreach from=$parent.items item="item"} <tr class="{if $item.label == 'Total'}total{/if} {if $item.archived}archived{/if}"> <th>{$item.label}{if $item.archived} <em>(archivé)</em>{/if}</th> <td> {if !$table_export} <span class="noprint"> <a href="{$admin_url}acc/reports/graphs.php?project={$item.id_project}&year={$item.id_year}">Graphiques</a> | <a href="{$admin_url}acc/reports/trial_balance.php?project={$item.id_project}&year={$item.id_year}">Balance générale</a> | <a href="{$admin_url}acc/reports/journal.php?project={$item.id_project}&year={$item.id_year}">Journal général</a> | <a href="{$admin_url}acc/reports/ledger.php?project={$item.id_project}&year={$item.id_year}">Grand livre</a> | <a href="{$admin_url}acc/reports/statement.php?project={$item.id_project}&year={$item.id_year}">Compte de résultat</a> | <a href="{$admin_url}acc/reports/balance_sheet.php?project={$item.id_project}&year={$item.id_year}">Bilan</a> </span> {/if} </td> <td class="money">{$item.sum_expense|raw|money}</td> <td class="money">{$item.sum_revenue|raw|money}</td> <td class="money">{$item.debit|raw|money:false}</td> <td class="money">{$item.credit|raw|money:false}</td> <td class="money">{$item.sum|raw|money:false}</td> </tr> {/foreach} </tbody> {/foreach} </table> | > > | 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 | <p class="actions"> {linkbutton href="!acc/reports/ledger.php?projects_only=1&year=%d"|args:$parent.id_year label="Grand livre analytique"} </p> {/if} </th> </tr> {foreach from=$parent.items item="item"} <?php $result = $item->sum_revenue - $item->sum_expense; ?> <tr class="{if $item.label == 'Total'}total{/if} {if $item.archived}archived{/if}"> <th>{$item.label}{if $item.archived} <em>(archivé)</em>{/if}</th> <td> {if !$table_export} <span class="noprint"> <a href="{$admin_url}acc/reports/graphs.php?project={$item.id_project}&year={$item.id_year}">Graphiques</a> | <a href="{$admin_url}acc/reports/trial_balance.php?project={$item.id_project}&year={$item.id_year}">Balance générale</a> | <a href="{$admin_url}acc/reports/journal.php?project={$item.id_project}&year={$item.id_year}">Journal général</a> | <a href="{$admin_url}acc/reports/ledger.php?project={$item.id_project}&year={$item.id_year}">Grand livre</a> | <a href="{$admin_url}acc/reports/statement.php?project={$item.id_project}&year={$item.id_year}">Compte de résultat</a> | <a href="{$admin_url}acc/reports/balance_sheet.php?project={$item.id_project}&year={$item.id_year}">Bilan</a> </span> {/if} </td> <td class="money">{$item.sum_expense|raw|money}</td> <td class="money">{$item.sum_revenue|raw|money}</td> <td class="money">{$result|raw|money:true:true}</td> <td class="money">{$item.debit|raw|money:false}</td> <td class="money">{$item.credit|raw|money:false}</td> <td class="money">{$item.sum|raw|money:false}</td> </tr> {/foreach} </tbody> {/foreach} </table> |
Modified src/templates/acc/search.tpl from [aaa89dd2e8] to [b5d5fc0074].
1 2 3 4 5 6 7 8 9 | {include file="_head.tpl" title="Recherche" current="acc" custom_js=['lib/query_builder.min.js']} <nav class="tabs"> <ul> <li class="current"><a href="{$self_url}">Recherche</a></li> <li><a href="saved_searches.php">Recherches enregistrées</a></li> </ul> </nav> | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | {include file="_head.tpl" title="Recherche" current="acc" custom_js=['lib/query_builder.min.js']} <nav class="tabs"> <ul> <li class="current"><a href="{$self_url}">Recherche</a></li> <li><a href="saved_searches.php">Recherches enregistrées</a></li> </ul> </nav> <form method="post" action="{$self_url_no_qs}" id="queryBuilderForm" data-disable-progress="1"> {include file="common/search/advanced.tpl"} {if $list !== null} <p class="help">{$list->count()} écritures trouvées pour cette recherche.</p> {if $list->count() > 0} |
︙ | ︙ |
Modified src/templates/acc/transactions/creator.tpl from [7581f116ec] to [8d3d1b3556].
|
| | > > > > | 1 2 3 4 5 6 7 8 9 | {include file="_head.tpl" title="Écritures créées par %s"|args:$transaction_creator->name() current="acc/accounts"} <p class="help"> De la plus récente à la plus ancienne. </p> {include file="acc/reports/_journal.tpl"} {include file="_foot.tpl"} |
Modified src/templates/acc/transactions/details.tpl from [59fa00da2b] to [690984355c].
︙ | ︙ | |||
106 107 108 109 110 111 112 | {if $transaction.type != $transaction::TYPE_ADVANCED} <dt>Référence de paiement</dt> <dd>{if $ref = $transaction->getPaymentReference()}<mark><a href="{$admin_url}payments.php?id={$transaction->reference|intval}">{$ref}</a></mark>{else}—{/if}</dd> <dt>Projet</dt> <dd> {if $project = $transaction->getProject()} | | | 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | {if $transaction.type != $transaction::TYPE_ADVANCED} <dt>Référence de paiement</dt> <dd>{if $ref = $transaction->getPaymentReference()}<mark><a href="{$admin_url}payments.php?id={$transaction->reference|intval}">{$ref}</a></mark>{else}—{/if}</dd> <dt>Projet</dt> <dd> {if $project = $transaction->getProject()} <mark class="variant-a">{link href="!acc/reports/statement.php?project=%d&year=%d"|args:$project.id:$transaction.id_year label=$project.name}</mark> {else} — {/if} {/if} <dt>Exercice</dt> <dd> |
︙ | ︙ | |||
206 207 208 209 210 211 212 | <td>{$line.account_label}</td> <td class="money">{if $line.debit}{$line.debit|escape|money}{/if}</td> <td class="money">{if $line.credit}{$line.credit|escape|money}{/if}</td> <td>{$line.label}</td> <td>{$line.reference}</td> <td> {if $line.id_project} | | | 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | <td>{$line.account_label}</td> <td class="money">{if $line.debit}{$line.debit|escape|money}{/if}</td> <td class="money">{if $line.credit}{$line.credit|escape|money}{/if}</td> <td>{$line.label}</td> <td>{$line.reference}</td> <td> {if $line.id_project} {link href="!acc/reports/statement.php?project=%d&year=%d"|args:$line.id_project:$transaction.id_year label=$line.project_name} {/if} </td> </tr> {/foreach} </tbody> </table> {/if} |
︙ | ︙ |
Modified src/templates/acc/transactions/pending.tpl from [362b758a05] to [6fdc7ef42c].
|
| | | 1 2 3 4 5 6 7 8 | {include file="_head.tpl" title="Dettes et créances non réglées sur les exercices clos" current="acc/simple"} <nav class="tabs"> <aside> {exportmenu} {linkbutton shape="search" href="!acc/search.php" label="Recherche"} </aside> </nav> |
︙ | ︙ | |||
35 36 37 38 39 40 41 | </tr> {/foreach} </tbody> </table> </form> | | | | 35 36 37 38 39 40 41 42 43 44 45 | </tr> {/foreach} </tbody> </table> </form> {$list->getHTMLPagination()|raw} {/if} {include file="_foot.tpl"} |
Modified src/templates/acc/transactions/user.tpl from [9299a647a1] to [6a563c7bfe].
|
| | > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | {include file="_head.tpl" title="Écritures liées à %s"|args:$transaction_user->name() current="acc/accounts"} {if !$dialog} <p> {linkbutton href="!users/details.php?id=%d"|args:$transaction_user.id label="Retour à la fiche membre" shape="user"} </p> {/if} <p class="help"> De la plus récente à la plus ancienne. </p> {include file="acc/reports/_journal.tpl"} <h2 class="ruler">Solde des comptes</h2> <form method="get" action="{$self_url_no_qs}"> <fieldset> |
︙ | ︙ |
Modified src/templates/config/users/index.tpl from [0ebae744b8] to [3cc877ef94].
︙ | ︙ | |||
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | <p class="help"> Les actions de création, modification ou suppression dans la base de données peuvent être enregistrées pour chaque membre. Cela permet de garder une trace, pour savoir qui à fait quoi. </p> <dl> {input type="select" options=$log_retention_options source=$config name="log_retention" required=true label="Durée de conservation des journaux d'activité" help="Après ce délai, les journaux seront supprimés."} </dl> </fieldset> <p class="submit"> {csrf_field key=$csrf_key} {button type="submit" name="save" label="Enregistrer" shape="right" class="main"} </p> </form> {include file="_foot.tpl"} | > > > > > > > > > > > | 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 | <p class="help"> Les actions de création, modification ou suppression dans la base de données peuvent être enregistrées pour chaque membre. Cela permet de garder une trace, pour savoir qui à fait quoi. </p> <dl> {input type="select" options=$log_retention_options source=$config name="log_retention" required=true label="Durée de conservation des journaux d'activité" help="Après ce délai, les journaux seront supprimés."} </dl> </fieldset> <fieldset> <legend>Sécurité</legend> <dl> {input type="select" name="auto_logout" source=$config required=true label="Déconnecter automatiquement les membres inactifs après…" options=$logout_delay_options} <dd class="help"> Permet de déconnecter automatiquement un membre s'il garde la gestion de l'association ouverte, sans interagir.<br /> Utile par exemple pour éviter de laisser une session ouverte sur un ordinateur partagé.<br />Ce réglage ne s'applique pas aux membres ayant coché la case "Rester connecté⋅e". </dd> </dl> </fieldset> <p class="submit"> {csrf_field key=$csrf_key} {button type="submit" name="save" label="Enregistrer" shape="right" class="main"} </p> </form> {include file="_foot.tpl"} |
Modified src/templates/docs/index.tpl from [7981801dbc] to [53e858fef5].
︙ | ︙ | |||
18 19 20 21 22 23 24 | <nav class="tabs"> <aside> <form method="post" action="search.php" target="_dialog" data-disable-progress="1"> {input type="text" name="q" size=25 placeholder="Rechercher un document" title="Rechercher dans les documents"} {button shape="search" type="submit" title="Rechercher"} </form> | > | | | | > | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | <nav class="tabs"> <aside> <form method="post" action="search.php" target="_dialog" data-disable-progress="1"> {input type="text" name="q" size=25 placeholder="Rechercher un document" title="Rechercher dans les documents"} {button shape="search" type="submit" title="Rechercher"} </form> {if !$list || !($list instanceof \Garradin\DynamicList)} {if $gallery} {linkbutton shape="menu" label="Afficher en liste" href="?path=%s&gallery=0"|args:$parent_path_uri} {else} {linkbutton shape="gallery" label="Afficher en galerie" href="?path=%s&gallery=1"|args:$parent_path_uri} {/if} {/if} {if $parent->canCreateDirHere() || $parent->canCreateHere()} {linkmenu label="Ajouter…" shape="plus" right=true} {if $parent->canCreateHere()} {linkbutton shape="upload" label="Depuis mon ordinateur" target="_dialog" href="!common/files/upload.php?p=%s"|args:$parent_path_uri} {if $parent->canCreateDirHere()} {linkbutton shape="folder" label="Répertoire" target="_dialog" href="!docs/new_dir.php?path=%s"|args:$parent_path_uri} |
︙ | ︙ |
Modified src/templates/me/index.tpl from [78eaa6c657] to [be51edf18c].
1 2 3 4 5 6 7 8 9 10 | {include file="_head.tpl" title="Mes informations personnelles" current="me"} {include file="./_nav.tpl" current="me"} {if $ok !== null} <p class="confirm block"> Les modifications ont bien été enregistrées. </p> {/if} | < > > > > > > > > > > > > > > > > > | 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 | {include file="_head.tpl" title="Mes informations personnelles" current="me"} {include file="./_nav.tpl" current="me"} {if $ok !== null} <p class="confirm block"> Les modifications ont bien été enregistrées. </p> {/if} <dl class="describe"> <dd>{linkbutton href="!me/edit.php" label="Modifier mes informations" shape="edit"}</dd> </dl> {if $user->isChild() || count($children)} <aside class="describe"> <dl class="describe"> {if $user->isChild()} <dt>Membre responsable</dt> <dd>{$parent_name}</dd> {elseif count($children)} <dt>Membres rattachés</dt> {foreach from=$children item="child"} <dd>{$child.name}</dd> {/foreach} {/if} </dl> </aside> {/if} {include file="users/_details.tpl" data=$user show_message_button=false mode="user"} <dl class="describe"> <dd>{linkbutton href="!me/export.php" label="Télécharger toutes les données détenues sur moi" shape="download"}</dd> </dl> {$snippets|raw} {include file="_foot.tpl"} |
Modified src/templates/me/services.tpl from [70f530026a] to [f15aece554].
︙ | ︙ | |||
72 73 74 75 76 77 78 79 80 | {/foreach} </tbody> </table> {$list->getHTMLPagination()|raw} {/if} {include file="_foot.tpl"} | > > | 72 73 74 75 76 77 78 79 80 81 82 | {/foreach} </tbody> </table> {$list->getHTMLPagination()|raw} {/if} {$snippets|raw} {include file="_foot.tpl"} |
Modified src/templates/users/index.tpl from [57040f9dcd] to [2c26d3ade1].
︙ | ︙ | |||
9 10 11 12 13 14 15 | {if $_GET.msg == 'DELETE'} <p class="block confirm">Le membre a été supprimé.</p> {elseif $_GET.msg == 'CATEGORY_CHANGED'} <p class="block confirm">Les membres sélectionnés ont bien été changés de catégorie.</p> {/if} {if !empty($categories)} | < | > > > | < > > > > > > > > > < | | 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 | {if $_GET.msg == 'DELETE'} <p class="block confirm">Le membre a été supprimé.</p> {elseif $_GET.msg == 'CATEGORY_CHANGED'} <p class="block confirm">Les membres sélectionnés ont bien été changés de catégorie.</p> {/if} {if !empty($categories)} <fieldset class="shortFormRight"> <legend>Filtrer par catégorie</legend> <nav class="dropdown"> <ul> <li><a></a></li> {foreach from=$categories key="c" item="category"} <li class="{if $c === $current_cat}selected{/if}"> <a href="?cat={$c}"> <strong>{$category.label}</strong> <small>{{%n membre}{%n membres} n=$category.count}</small> </a> </li> {/foreach} </ul> </nav> </fieldset> {/if} <form method="get" action="search.php" class="shortFormLeft" data-focus="1"> <fieldset> <legend>Rechercher un membre</legend> <input type="text" name="qt" value="" placeholder="Nom, numéro, ou adresse e-mail" /> {button type="submit" name="" title="Chercher" shape="search"} </fieldset> </form> <form method="post" action="action.php" class="users-list" target="_dialog"> {if $list->count()} {$list->getHTMLPagination()|raw} |
︙ | ︙ | |||
46 47 48 49 50 51 52 53 54 55 56 57 58 59 | <?php $value = $row->$key; ?> {if $key == 'number'} <td class="num"> {link href="details.php?id=%d"|args:$row._user_id label=$value} </td> {elseif $key == 'identity'} <th>{link href="details.php?id=%d"|args:$row._user_id label=$value}</th> {else} <td> {display_dynamic_field key=$key value=$value user_id=$row._user_id thumb_url="details.php?id=%d"|args:$row._user_id} </td> {/if} {/foreach} | > > > > > > > > > > > > | 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 | <?php $value = $row->$key; ?> {if $key == 'number'} <td class="num"> {link href="details.php?id=%d"|args:$row._user_id label=$value} </td> {elseif $key == 'identity'} <th>{link href="details.php?id=%d"|args:$row._user_id label=$value}</th> {elseif $key == 'id_parent'} <td> {if $value} {link href="details.php?id=%d"|args:$value label=$row._parent_name} {/if} </td> {elseif $key == 'is_parent'} <td> {if $value} Oui {/if} </td> {else} <td> {display_dynamic_field key=$key value=$value user_id=$row._user_id thumb_url="details.php?id=%d"|args:$row._user_id} </td> {/if} {/foreach} |
︙ | ︙ |
Modified src/templates/users/search.tpl from [4b65783723] to [a9b19a499c].
1 2 3 4 | {include file="_head.tpl" title="Recherche de membre" current="users" custom_js=['lib/query_builder.min.js']} {include file="users/_nav.tpl" current="search"} | | < < < < > > > | 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 | {include file="_head.tpl" title="Recherche de membre" current="users" custom_js=['lib/query_builder.min.js']} {include file="users/_nav.tpl" current="search"} <form method="post" action="{$self_url_no_qs}" id="queryBuilderForm" data-disable-progress="1"> {include file="common/search/advanced.tpl"} {if $list !== null} <p class="help">{$list->count()} membres trouvés pour cette recherche.</p> {if $list->count() > 0} <p class="actions">{exportmenu form=true name="_dl_export" class="menu-btn-right"}</p> {/if} </form> <form method="post" action="action.php" target="_dialog"> {include file="common/dynamic_list_head.tpl" check=$is_admin use_buttons=true} {foreach from=$list->iterate() item="row"} <tr> {if $is_admin} <td class="check">{input type="checkbox" name="selected[]" value=$row.id}</td> {/if} |
︙ | ︙ | |||
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 | </tr> {/foreach} </tbody> {if $is_admin} {include file="users/_list_actions.tpl" colspan=$list->countHeaderColumns()+1} {/if} </table> {$list->getHTMLPagination(true)|raw} {elseif $results} <p class="actions">{exportmenu form=true name="_export" class="menu-btn-right"}</p> <?php $id_column = array_search('_user_id', $header, true); if (false === $id_column) { $id_column = array_search('id', $header, true); } $header_count = count($header); ?> <table class="list"> <thead> <tr> {if $is_admin && $id_column !== false} <td class="check"><input type="checkbox" title="Tout cocher / décocher" id="f_all" /><label for="f_all"></label></td> {/if} {foreach from=$header item="column"} | > > > > > | 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 | </tr> {/foreach} </tbody> {if $is_admin} {include file="users/_list_actions.tpl" colspan=$list->countHeaderColumns()+1} {/if} </table> </form> {$list->getHTMLPagination(true)|raw} {elseif $results} <p class="actions">{exportmenu form=true name="_export" class="menu-btn-right"}</p> <?php $id_column = array_search('_user_id', $header, true); if (false === $id_column) { $id_column = array_search('id', $header, true); } $header_count = count($header); ?> </form> <form method="post" action="action.php" target="_dialog"> <table class="list"> <thead> <tr> {if $is_admin && $id_column !== false} <td class="check"><input type="checkbox" title="Tout cocher / décocher" id="f_all" /><label for="f_all"></label></td> {/if} {foreach from=$header item="column"} |
︙ | ︙ | |||
87 88 89 90 91 92 93 94 95 96 97 98 99 | {/foreach} </tbody> {if $is_admin && $id_column !== false} {include file="users/_list_actions.tpl" colspan=$header_count+1} {/if} </table> {/if} </form> {include file="_foot.tpl"} | > > | 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | {/foreach} </tbody> {if $is_admin && $id_column !== false} {include file="users/_list_actions.tpl" colspan=$header_count+1} {/if} </table> </form> {/if} </form> {include file="_foot.tpl"} |
Modified src/www/admin/_inc.php from [2059b7f42e] to [926355331c].
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?php namespace Garradin; use Garradin\Users\Session; require_once __DIR__ . '/../../include/init.php'; function f($key) { return \KD2\Form::get($key); } | < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?php namespace Garradin; use Garradin\Users\Session; require_once __DIR__ . '/../../include/init.php'; function f($key) { return \KD2\Form::get($key); } function qg($key) { return isset($_GET[$key]) ? $_GET[$key] : null; } $tpl = Template::getInstance(); |
︙ | ︙ |
Modified src/www/admin/acc/reports/graph_plot_all.php from [cdc72313d9] to [42754fd13a].
1 2 3 4 5 6 7 | <?php namespace Garradin; use Garradin\Accounting\Graph; require_once __DIR__ . '/../_inc.php'; | > | > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?php namespace Garradin; use Garradin\Accounting\Graph; require_once __DIR__ . '/../_inc.php'; $type = $_GET['type'] ?? null; if (!$type) { throw new UserException('Missing type'); } header('Content-Type: image/svg+xml'); $expiry = time() - 30; $hash = sha1('graph_plot_all'); if (!Utils::HTTPCache($hash, $expiry)) { echo Graph::bar($type, []); } |
Modified src/www/admin/acc/transactions/creator.php from [3ffff6389f] to [e16800c5f0].
︙ | ︙ | |||
12 13 14 15 16 17 18 | if (!$u) { throw new UserException('Ce membre n\'existe pas'); } $criterias = ['creator' => $u->id]; | | | 12 13 14 15 16 17 18 19 20 21 22 | if (!$u) { throw new UserException('Ce membre n\'existe pas'); } $criterias = ['creator' => $u->id]; $tpl->assign('journal', Reports::getJournal($criterias, true)); $tpl->assign('transaction_creator', $u); $tpl->display('acc/transactions/creator.tpl'); |
Modified src/www/admin/acc/transactions/user.php from [4e7ffb8f16] to [7e8decf9cc].
︙ | ︙ | |||
18 19 20 21 22 23 24 | $years = Years::listAssoc(); end($years); $year = (int)qg('year') ?: key($years); $criterias = ['user' => $u->id]; $tpl->assign('balance', Reports::getAccountsBalances($criterias + ['year' => $year], null, false)); | | | 18 19 20 21 22 23 24 25 26 27 28 29 | $years = Years::listAssoc(); end($years); $year = (int)qg('year') ?: key($years); $criterias = ['user' => $u->id]; $tpl->assign('balance', Reports::getAccountsBalances($criterias + ['year' => $year], null, false)); $tpl->assign('journal', Reports::getJournal($criterias, true)); $tpl->assign(compact('years', 'year')); $tpl->assign('transaction_user', $u); $tpl->display('acc/transactions/user.tpl'); |
Modified src/www/admin/common/files/edit.php from [c26e5bfe74] to [5ab1c9475d].
︙ | ︙ | |||
30 31 32 33 34 35 36 | $file->setContent(f('content')); if (qg('js') !== null) { die('{"success":true}'); } }, $csrf_key, Utils::getSelfURI()); | | > > > > > > | 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | $file->setContent(f('content')); if (qg('js') !== null) { die('{"success":true}'); } }, $csrf_key, Utils::getSelfURI()); $tpl->assign(compact('csrf_key', 'file')); $fallback = qg('fallback'); if (!$editor && $fallback) { $editor = $fallback; } if (!$editor) { $tpl->display('common/files/upload.tpl'); } elseif ($editor == 'wopi') { echo $file->editorHTML(); } |
︙ | ︙ |
Modified src/www/admin/common/saved_searches.php from [d2528299b7] to [95f81abb85].
︙ | ︙ | |||
23 24 25 26 27 28 29 | { $s = Search::get(qg('edit') ?: qg('delete')); if (!$s) { throw new UserException('Recherche non trouvée'); } | | | 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | { $s = Search::get(qg('edit') ?: qg('delete')); if (!$s) { throw new UserException('Recherche non trouvée'); } if ($s->id_user !== null && $s->id_user != Session::getInstance()->getUser()->id) { throw new UserException('Recherche privée appartenant à un autre membre.'); } $csrf_key = 'search_' . $s->id; $form->runIf('save', function () use ($s) { $s->importForm(); |
︙ | ︙ |
Modified src/www/admin/config/users/index.php from [ff4d06feca] to [b4de2ccc34].
︙ | ︙ | |||
36 37 38 39 40 41 42 43 44 45 46 47 48 | 0 => 'Ne pas enregistrer de journaux', 7 => 'Une semaine', 30 => 'Un mois', 90 => '3 mois', 180 => '6 mois', 365 => 'Un an', 720 => 'Deux ans', ], ]); $tpl->assign(compact('csrf_key', 'config')); $tpl->display('config/users/index.tpl'); | > > > > > > > > > > | 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | 0 => 'Ne pas enregistrer de journaux', 7 => 'Une semaine', 30 => 'Un mois', 90 => '3 mois', 180 => '6 mois', 365 => 'Un an', 720 => 'Deux ans', ], 'logout_delay_options' => [ 0 => 'Pas de déconnexion automatique', 1 => '1 minute', 15 => '15 minutes', 30 => '30 minutes', 60 => '1 heure', 2*60 => '2 heures', 3*60 => '3 heures', 6*60 => '6 heures', ], ]); $tpl->assign(compact('csrf_key', 'config')); $tpl->display('config/users/index.tpl'); |
Modified src/www/admin/docs/new_file.php from [2cd6b3a91e] to [7a1d38b8f4].
︙ | ︙ | |||
20 21 22 23 24 25 26 | if (!strpos($name, '.')) { $name .= '.md'; } $file = Files::createFromString($parent . '/' . $name, ''); | | | 20 21 22 23 24 25 26 27 28 29 30 31 32 | if (!strpos($name, '.')) { $name .= '.md'; } $file = Files::createFromString($parent . '/' . $name, ''); Utils::redirect('!common/files/edit.php?fallback=code&p=' . rawurlencode($file->path)); }, $csrf_key); $tpl->assign(compact('csrf_key')); $tpl->display('docs/new_file.tpl'); |
Modified src/www/admin/me/index.php from [59b3150bbd] to [1d5c7770f1].
1 2 3 4 5 6 7 8 9 | <?php namespace Garradin; use Garradin\UserTemplate\Modules; require_once __DIR__ . '/_inc.php'; $ok = qg('ok'); | | > | | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <?php namespace Garradin; use Garradin\UserTemplate\Modules; require_once __DIR__ . '/_inc.php'; $ok = qg('ok'); $parent_name = $user->getParentName(); $children = $user->listChildren(); $variables = compact('user', 'parent_name', 'children', 'ok'); $tpl->assign('snippets', Modules::snippetsAsString(Modules::SNIPPET_MY_DETAILS, $variables)); $tpl->assign($variables); $tpl->display('me/index.tpl'); |
Modified src/www/admin/me/services.php from [e9cf9078c4] to [7a18b9b262].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php namespace Garradin; use Garradin\Services\Services_User; use Garradin\Accounting\Reports; use Garradin\Entities\Accounting\Account; require_once __DIR__ . '/_inc.php'; $tpl->assign('membre', $user); $list = Services_User::perUserList($user->id); $list->loadFromQueryString(); $tpl->assign(compact('list')); | > | | > > > > | 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 | <?php namespace Garradin; use Garradin\Services\Services_User; use Garradin\Accounting\Reports; use Garradin\Entities\Accounting\Account; use Garradin\UserTemplate\Modules; require_once __DIR__ . '/_inc.php'; $tpl->assign('membre', $user); $list = Services_User::perUserList($user->id); $list->loadFromQueryString(); $tpl->assign(compact('list')); $services = Services_User::listDistinctForUser($user->id); $accounts = Reports::getAccountsBalances(['user' => $user->id, 'type' => Account::TYPE_THIRD_PARTY]); $variables = compact('list', 'services', 'accounts'); $tpl->assign($variables); $tpl->assign('snippets', Modules::snippetsAsString(Modules::SNIPPET_MY_SERVICES, $variables)); $tpl->display('me/services.tpl'); |
Modified src/www/admin/static/doc/brindille_sections.html from [7038562b44] to [cab7dda856].
︙ | ︙ | |||
71 72 73 74 75 76 77 78 79 80 81 82 83 84 | <li><a href="#users">users</a></li> <li><a href="#subscriptions">subscriptions</a> </ol></li> <li><a href="#comptabilite">Comptabilité</a> <ol> <li><a href="#accounts">accounts</a></li> <li><a href="#balances">balances</a></li> <li><a href="#years">years</a> </ol></li> <li><a href="#pour-le-site-web">Pour le site web</a> <ol> <li><a href="#breadcrumbs">breadcrumbs</a></li> <li><a href="#pages-articles-categories-sup-sql-sup">pages, articles, categories <sup>(sql)</sup></a></li> <li><a href="#files-documents-images-sup-sql-sup">files, documents, images <sup>(sql)</sup></a> | > | 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | <li><a href="#users">users</a></li> <li><a href="#subscriptions">subscriptions</a> </ol></li> <li><a href="#comptabilite">Comptabilité</a> <ol> <li><a href="#accounts">accounts</a></li> <li><a href="#balances">balances</a></li> <li><a href="#transactions">transactions</a></li> <li><a href="#years">years</a> </ol></li> <li><a href="#pour-le-site-web">Pour le site web</a> <ol> <li><a href="#breadcrumbs">breadcrumbs</a></li> <li><a href="#pages-articles-categories-sup-sql-sup">pages, articles, categories <sup>(sql)</sup></a></li> <li><a href="#files-documents-images-sup-sql-sup">files, documents, images <sup>(sql)</sup></a> |
︙ | ︙ | |||
323 324 325 326 327 328 329 | <p>Il est également possible de passer des arguments dans les paramètres à l'aides des arguments nommés qui commencent par deux points <code>:</code> :</p> <pre><code>{{#articles where="title = :montitre" :montitre="Actualité"}}</code></pre> <h1 id="membres">Membres</h1> <h2 id="users">users</h2> <p>Liste les membres.</p> <p>Paramètres possibles :</p> <p>| <code>id</code> | optionnel | Identifiant unique du membre, ou tableau contenant une liste d'identifiants. |<br /> | | > > > > > > > > > > > > > | > > > > | > > > > | > > > | 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 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 | <p>Il est également possible de passer des arguments dans les paramètres à l'aides des arguments nommés qui commencent par deux points <code>:</code> :</p> <pre><code>{{#articles where="title = :montitre" :montitre="Actualité"}}</code></pre> <h1 id="membres">Membres</h1> <h2 id="users">users</h2> <p>Liste les membres.</p> <p>Paramètres possibles :</p> <p>| <code>id</code> | optionnel | Identifiant unique du membre, ou tableau contenant une liste d'identifiants. |<br /> | <code>search_name</code> | optionnel | Ne lister que les membres dont le nom correspond au texte passé en paramètre. |<br /> | <code>id_parent</code> | optionnel | Ne lister que les membres rattachés à l'identifiant unique du membre responsable indiqué. |</p> <p>Chaque itération renverra la fiche du membre, ainsi que ces variables :</p> <p>| <code>$id</code> | Identifiant unique du membre |<br /> | <code>$_name</code> | Nom du membre, tel que défini dans la configuration |<br /> | <code>$_login</code> | Identifiant de connexion du membre, tel que défini dans la configuration |<br /> | <code>$_number</code> | Numéro du membre, tel que défini dans la configuration |</p> <h2 id="subscriptions">subscriptions</h2> <p>Liste les inscriptions à une ou des activités.</p> <p>Paramètres possibles :</p> <table> <thead> <tr> <th style="text-align: left;">Paramètre</th> <th style="text-align: left;"></th> <th style="text-align: left;">Fonction</th> </tr> </thead> <tbody> <tr> <td style="text-align: left;"><code>user</code></td> <td style="text-align: left;">optionnel</td> <td style="text-align: left;">Identifiant unique du membre</td> </tr> <tr> <td style="text-align: left;"><code>active</code></td> <td style="text-align: left;">optionnel</td> <td style="text-align: left;">Si <code>TRUE</code>, seules les inscriptions à jour sont listées</td> </tr> <tr> <td style="text-align: left;"><code>id_service</code></td> <td style="text-align: left;">optionnel</td> <td style="text-align: left;">Ne renvoie que les inscriptions à l'activité correspondant à cet ID.</td> </tr> </tbody> </table> <h1 id="comptabilite">Comptabilité</h1> <h2 id="accounts">accounts</h2> <p>Liste les comptes d'un plan comptable.</p> <table> <thead> <tr> <th style="text-align: left;">Paramètre</th> |
︙ | ︙ | |||
372 373 374 375 376 377 378 | <tbody> <tr> <td style="text-align: left;"><code>codes</code> (optionel)</td> <td style="text-align: left;">Ne renvoyer que les balances des comptes ayant ces codes (séparer par des virgules).</td> </tr> <tr> <td style="text-align: left;"><code>year</code> (optionel)</td> | | > > > > > > > > > > > > > > > > > > > > > > > | 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 | <tbody> <tr> <td style="text-align: left;"><code>codes</code> (optionel)</td> <td style="text-align: left;">Ne renvoyer que les balances des comptes ayant ces codes (séparer par des virgules).</td> </tr> <tr> <td style="text-align: left;"><code>year</code> (optionel)</td> <td style="text-align: left;">Ne renvoyer que les balances des comptes utilisés sur l'année (indiquer ici un ID de year).</td> </tr> </tbody> </table> <h2 id="transactions">transactions</h2> <p>Renvoie des écritures.</p> <table> <thead> <tr> <th style="text-align: left;">Paramètre</th> <th style="text-align: left;"></th> <th style="text-align: left;">Fonction</th> </tr> </thead> <tbody> <tr> <td style="text-align: left;"><code>id</code></td> <td style="text-align: left;">optionnel</td> <td style="text-align: left;">Indiquer un ID d'écriture pour récupérer ses informations.</td> </tr> <tr> <td style="text-align: left;"><code>user</code></td> <td style="text-align: left;">optionnel</td> <td style="text-align: left;">Indiquer ici un ID utilisateur pour lister les écritures liées à un membre.</td> </tr> </tbody> </table> <h2 id="years">years</h2> <p>Liste les exercices comptables</p> <table> <thead> |
︙ | ︙ |
Modified src/www/admin/static/doc/markdown.html from [684b12e976] to [47747ea422].
︙ | ︙ | |||
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | <li><a href="#identifiant-et-classe-css-sur-les-titres">Identifiant et classe CSS sur les titres</a></li> <li><a href="#classes-css">Classes CSS</a></li> <li><a href="#tags-html">Tags HTML</a> </ol></li> <li><a href="#extensions">Extensions</a> <ol> <li><a href="#images-jointes">Images jointes</a></li> <li><a href="#fichiers-joints">Fichiers joints</a></li> <li><a href="#sommaire-table-des-matieres-automatique">Sommaire / table des matières automatique</a> <ol> <li><a href="#exclure-un-sous-titre-du-sommaire">Exclure un sous-titre du sommaire</a> </ol></li> <li><a href="#grilles-et-colonnes">Grilles et colonnes</a></li> <li><a href="#alignement-du-texte">Alignement du texte</a></li> <li><a href="#couleurs">Couleurs</a> | > > > > > | 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | <li><a href="#identifiant-et-classe-css-sur-les-titres">Identifiant et classe CSS sur les titres</a></li> <li><a href="#classes-css">Classes CSS</a></li> <li><a href="#tags-html">Tags HTML</a> </ol></li> <li><a href="#extensions">Extensions</a> <ol> <li><a href="#images-jointes">Images jointes</a></li> <li><a href="#galerie-d-images">Galerie d'images</a> <ol> <li><a href="#diaporama-d-images">Diaporama d'images</a> </ol></li> <li><a href="#fichiers-joints">Fichiers joints</a></li> <li><a href="#videos">Vidéos</a></li> <li><a href="#sommaire-table-des-matieres-automatique">Sommaire / table des matières automatique</a> <ol> <li><a href="#exclure-un-sous-titre-du-sommaire">Exclure un sous-titre du sommaire</a> </ol></li> <li><a href="#grilles-et-colonnes">Grilles et colonnes</a></li> <li><a href="#alignement-du-texte">Alignement du texte</a></li> <li><a href="#couleurs">Couleurs</a> |
︙ | ︙ | |||
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 | <li>Légende : indiquer ici une courte description de l'image.</li> </ul> <p>Exemple :</p> <pre><code><<image|mon_image.png|center|Ceci est une belle image>></code></pre> <p>Il est aussi possible d'utiliser la syntaxe avec des paramètres nommés :</p> <pre><code><<image file="Nom_fichier.jpg" align="center" caption="Légende">></code></pre> <p>Les images qui ne sont pas mentionnées dans le texte seront affichées après le texte sous forme de galerie.</p> <h2 id="fichiers-joints">Fichiers joints</h2> <p>Pour créer un bouton permettant de voir ou télécharger un fichier joint à la page web, il suffit d'utiliser la syntaxe suivante :</p> <pre><code><<file|Nom_fichier.ext|Libellé>></code></pre> <ul> <li><code>Nom_fichier.ext</code> : remplacer par le nom du fichier (parmi les fichiers joints à la page)</li> <li><code>Libellé</code> : indique le libellé du qui sera affiché sur le bouton, si aucun libellé n'est indiqué alors c'est le nom du fichier qui sera affiché</li> </ul> <h2 id="sommaire-table-des-matieres-automatique">Sommaire / table des matières automatique</h2> <p>Il est possible de placer le code <code><<toc>></code> pour générer un sommaire automatiquement à partir des titres et sous-titres :</p> <pre><code><<toc>></code></pre> <p>Affichera un sommaire comme celui-ci :</p><div class="toc"> <ol> <li><a href="#syntaxe-markdown">Syntaxe MarkDown</a> <ol> | > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 | <li>Légende : indiquer ici une courte description de l'image.</li> </ul> <p>Exemple :</p> <pre><code><<image|mon_image.png|center|Ceci est une belle image>></code></pre> <p>Il est aussi possible d'utiliser la syntaxe avec des paramètres nommés :</p> <pre><code><<image file="Nom_fichier.jpg" align="center" caption="Légende">></code></pre> <p>Les images qui ne sont pas mentionnées dans le texte seront affichées après le texte sous forme de galerie.</p> <h2 id="galerie-d-images">Galerie d'images</h2> <p>Il est possible d'afficher une galerie d'images (sous forme d'images miniatures) avec la balise <code><<gallery</code> qui contient la liste des images à mettre dans la galerie :</p> <pre><code><<gallery Nom_fichier.jpg Nom_fichier_2.jpg >></code></pre> <p>Si aucun nom de fichier n'est indiqué, alors toutes les images jointes à la page seront affichées :</p> <pre><code><<gallery>></code></pre> <h3 id="diaporama-d-images">Diaporama d'images</h3> <p>On peut également afficher cette galerie sous forme de diaporama. Dans ce cas une seule image est affichée, et on peut passer de l'une à l'autre.</p> <p>La syntaxe est la même, mais on ajoute le mot <code>slideshow</code> après le mot <code>gallery</code> :</p> <pre><code><<gallery slideshow Nom_fichier.jpg Nom_fichier_2.jpg >></code></pre> <h2 id="fichiers-joints">Fichiers joints</h2> <p>Pour créer un bouton permettant de voir ou télécharger un fichier joint à la page web, il suffit d'utiliser la syntaxe suivante :</p> <pre><code><<file|Nom_fichier.ext|Libellé>></code></pre> <ul> <li><code>Nom_fichier.ext</code> : remplacer par le nom du fichier (parmi les fichiers joints à la page)</li> <li><code>Libellé</code> : indique le libellé du qui sera affiché sur le bouton, si aucun libellé n'est indiqué alors c'est le nom du fichier qui sera affiché</li> </ul> <h2 id="videos">Vidéos</h2> <p>Pour inclure un lecteur vidéo dans la page web à partir d'un fichier vidéo joint à la page, il faut utiliser le code suivant :</p> <pre><code><<video|Nom_du_fichier.ext>></code></pre> <p>On peut aussi spécifier d'autres paramètres :</p> <ul> <li><code>file</code> : nom du fichier vidéo</li> <li><code>poster</code> : nom de fichier d'une image utilisée pour remplacer la vidéo avant qu'elle ne soit lue</li> <li><code>subtitles</code> : nom d'un fichier de sous-titres au format VTT (le format SRT n'est pas géré par les navigateurs)</li> <li><code>width</code> : largeur de la vidéo (en pixels)</li> <li><code>height</code> : hauteur de la vidéo (en pixels)</li> </ul> <p>Exemple :</p> <pre><code><<video file="Ma_video.webm" poster="Ma_video_poster.jpg" width="640" height="360" subtitles="Ma_video_sous_titres.vtt">></code></pre> <h2 id="sommaire-table-des-matieres-automatique">Sommaire / table des matières automatique</h2> <p>Il est possible de placer le code <code><<toc>></code> pour générer un sommaire automatiquement à partir des titres et sous-titres :</p> <pre><code><<toc>></code></pre> <p>Affichera un sommaire comme celui-ci :</p><div class="toc"> <ol> <li><a href="#syntaxe-markdown">Syntaxe MarkDown</a> <ol> |
︙ | ︙ | |||
473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 | <li><a href="#identifiant-et-classe-css-sur-les-titres">Identifiant et classe CSS sur les titres</a></li> <li><a href="#classes-css">Classes CSS</a></li> <li><a href="#tags-html">Tags HTML</a> </ol></li> <li><a href="#extensions">Extensions</a> <ol> <li><a href="#images-jointes">Images jointes</a></li> <li><a href="#fichiers-joints">Fichiers joints</a></li> <li><a href="#sommaire-table-des-matieres-automatique">Sommaire / table des matières automatique</a> <ol> <li><a href="#exclure-un-sous-titre-du-sommaire">Exclure un sous-titre du sommaire</a> </ol></li> <li><a href="#grilles-et-colonnes">Grilles et colonnes</a></li> <li><a href="#alignement-du-texte">Alignement du texte</a></li> <li><a href="#couleurs">Couleurs</a> | > > > > > | 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 | <li><a href="#identifiant-et-classe-css-sur-les-titres">Identifiant et classe CSS sur les titres</a></li> <li><a href="#classes-css">Classes CSS</a></li> <li><a href="#tags-html">Tags HTML</a> </ol></li> <li><a href="#extensions">Extensions</a> <ol> <li><a href="#images-jointes">Images jointes</a></li> <li><a href="#galerie-d-images">Galerie d'images</a> <ol> <li><a href="#diaporama-d-images">Diaporama d'images</a> </ol></li> <li><a href="#fichiers-joints">Fichiers joints</a></li> <li><a href="#videos">Vidéos</a></li> <li><a href="#sommaire-table-des-matieres-automatique">Sommaire / table des matières automatique</a> <ol> <li><a href="#exclure-un-sous-titre-du-sommaire">Exclure un sous-titre du sommaire</a> </ol></li> <li><a href="#grilles-et-colonnes">Grilles et colonnes</a></li> <li><a href="#alignement-du-texte">Alignement du texte</a></li> <li><a href="#couleurs">Couleurs</a> |
︙ | ︙ |
Modified src/www/admin/static/scripts/accounting_setup.js from [008faf6f1e] to [11af0b9e26].
︙ | ︙ | |||
9 10 11 12 13 14 15 | return false; } row.parentNode.removeChild(row); }; } | > | | | | | | | | | | | | | > | 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 | return false; } row.parentNode.removeChild(row); }; } if ($('table').length) { $('tbody tr').forEach(initLine); // Add row "plus" button $('tfoot button')[0].onclick = () => { let lines = $('tbody tr'); var line = lines[lines.length - 1]; var n = line.cloneNode(true); // Reset label and reference n.querySelectorAll('input').forEach((i) => { i.value = ''; }) line.parentNode.appendChild(n); initLine(n); }; } |
Modified src/www/admin/static/scripts/global.js from [d9a85db429] to [ae35c2f06e].
︙ | ︙ | |||
95 96 97 98 99 100 101 | link.href = this.static_url + file + '?' + g.version; return document.head.appendChild(link); }; g.dialog = null; g.focus_before_dialog = null; | | > > > > > > > > > > > | | | | | | | > > > > < < | 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 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | link.href = this.static_url + file + '?' + g.version; return document.head.appendChild(link); }; g.dialog = null; g.focus_before_dialog = null; g.openDialog = function (content, options) { var close = true, callback = null, classname = null; if (typeof options === "object" && options !== null) { callback = options.callback ?? null; classname = options.classname ?? null; close = options.close ?? true; } if (null !== g.dialog) { g.closeDialog(); } g.focus_before_dialog = document.activeElement; g.dialog = document.createElement('dialog'); g.dialog.id = 'dialog'; g.dialog.open = true; g.dialog.className = classname || ''; if (close) { var btn = document.createElement('button'); btn.className = 'icn-btn closeBtn'; btn.setAttribute('data-icon', '✘'); btn.type = 'button'; btn.innerHTML = 'Fermer'; btn.onclick = g.closeDialog; g.dialog.appendChild(btn); g.dialog.onclick = (e) => { if (e.target == g.dialog) g.closeDialog(); }; window.onkeyup = (e) => { if (e.key == 'Escape') g.closeDialog(); }; } if (typeof content == 'string') { var container = document.createElement('div'); container.innerHTML = content; content = container; } else if (content instanceof DocumentFragment) { var container = document.createElement('div'); container.appendChild(content.cloneNode(true)); content = container; } g.dialog.appendChild(content); g.dialog.style.opacity = 0; let tag = content.tagName.toLowerCase(); if (tag == 'img' || tag == 'iframe') { event = 'load'; } else if (tag == 'audio' || tag == 'video') { |
︙ | ︙ | |||
160 161 162 163 164 165 166 | // Restore CSS defaults window.setTimeout(() => { g.dialog.style.opacity = ''; }, 50); return content; } | | > > > > > | 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | // Restore CSS defaults window.setTimeout(() => { g.dialog.style.opacity = ''; }, 50); return content; } g.openFrameDialog = function (url, options) { options ??= {}; var height = options.height || '90%'; var callback = options.callback || null; var classname = options.classname || null; var iframe = document.createElement('iframe'); iframe.src = url; iframe.name = 'dialog'; iframe.id = 'frameDialog'; iframe.frameborder = '0'; iframe.scrolling = 'yes'; iframe.width = iframe.height = 0; |
︙ | ︙ | |||
183 184 185 186 187 188 189 | // We need to wait a bit for the height to be correct, not sure why window.setTimeout(() => { iframe.style.height = iframe.dataset.height == 'auto' ? iframe.contentWindow.document.body.offsetHeight + 'px' : iframe.dataset.height; }, 200); }); | | | 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | // We need to wait a bit for the height to be correct, not sure why window.setTimeout(() => { iframe.style.height = iframe.dataset.height == 'auto' ? iframe.contentWindow.document.body.offsetHeight + 'px' : iframe.dataset.height; }, 200); }); g.openDialog(iframe, {callback, classname}); return iframe; }; g.reloadParentDialog = () => { if (!window.parent.g.dialog) { return; } |
︙ | ︙ | |||
515 516 517 518 519 520 521 | } if (location.href.match(/_dialog/)) { location.href = url; return false; } | | | 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 | } if (location.href.match(/_dialog/)) { location.href = url; return false; } g.openFrameDialog(url, {'height': e.getAttribute('data-dialog-height') || 'auto', 'classname': e.getAttribute('data-dialog-class')}); return false; } if (type.match(/^image\//)) { var i = document.createElement('img'); i.src = e.href; } |
︙ | ︙ | |||
537 538 539 540 541 542 543 | var i = document.createElement('video'); i.autoplay = true; i.controls = true; i.src = e.href; } else { let url = e.href + (e.href.indexOf('?') > 0 ? '&' : '?') + '_dialog'; | | | | 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 | var i = document.createElement('video'); i.autoplay = true; i.controls = true; i.src = e.href; } else { let url = e.href + (e.href.indexOf('?') > 0 ? '&' : '?') + '_dialog'; g.openFrameDialog(url, {height: '90%'}); return false; } g.openDialog(i); return false; }; }); $('form[target="_dialog"]').forEach((e) => { e.addEventListener('submit', () => { if (e.target != '_dialog' && e.target != 'dialog') return; let url = e.getAttribute('action'); url = url + (url.indexOf('?') > 0 ? '&' : '?') + '_dialog'; e.setAttribute('action', url); e.target = 'dialog'; g.openFrameDialog('about:blank', {height: e.getAttribute('data-dialog-height') ? 90 : 'auto'}); e.submit(); return false; }); }); }); g.onload(() => { |
︙ | ︙ |
Modified src/www/admin/static/scripts/service_form.js from [d3a768cf39] to [272a6811a7].
︙ | ︙ | |||
46 47 48 49 50 51 52 | } // Fill the amount paid by the user if (amount && create) { $('#f_amount').value = g.formatMoney(amount); } | | | 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | } // Fill the amount paid by the user if (amount && create) { $('#f_amount').value = g.formatMoney(amount); } if ('project' in elm.dataset) { $('#f_id_project').value = elm.dataset.project; } } function initForm() { $('input[name=id_service]').forEach((e) => { e.onchange = () => { selectService(e); }; |
︙ | ︙ |
Modified src/www/admin/static/scripts/web_editor.js from [7b56af47a3] to [a168ced166].
︙ | ︙ | |||
51 52 53 54 55 56 57 | }; // Warn before closing window if content was changed window.addEventListener('beforeunload', preventClose, { capture: true }); t.textarea.form.addEventListener('submit', () => { window.removeEventListener('beforeunload', preventClose, {capture: true}); | | | 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | }; // Warn before closing window if content was changed window.addEventListener('beforeunload', preventClose, { capture: true }); t.textarea.form.addEventListener('submit', () => { window.removeEventListener('beforeunload', preventClose, {capture: true}); save((data) => { localStorage.removeItem(backup_key); location.href = data.redirect; }); return false; }); // Cancel Escape to close.value if (window.parent && window.parent.g.dialog) { // Always fullscreen in dialogs config.fullscreen = true; |
︙ | ︙ | |||
133 134 135 136 137 138 139 | return true; }; var openFileInsert = function (callback) { let args = new URLSearchParams(window.location.search); var uri = args.get('p'); | | | | 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | return true; }; var openFileInsert = function (callback) { let args = new URLSearchParams(window.location.search); var uri = args.get('p'); g.openFrameDialog(g.admin_url + 'web/_attach.php?files&_dialog&p=' + uri, {callback}); return true; }; var openImageInsert = function (callback) { let args = new URLSearchParams(window.location.search); var uri = args.get('p'); g.openFrameDialog(g.admin_url + 'web/_attach.php?images&_dialog&p=' + uri, {callback}); return true; }; window.te_insertFile = function (file) { var tag = '<<file|'+file+'>>'; |
︙ | ︙ | |||
485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 | }); } window.setTimeout(() => { if ((v = localStorage.getItem(backup_key)) && v.trim() !== t.textarea.value.trim() && window.confirm(msg_restore)) { t.textarea.value = v; } }, 50); window.setInterval(() => { if (t.textarea.value.trim() === t.textarea.defaultValue.trim()) { return; } localStorage.setItem(backup_key, t.textarea.value); }, 10000); } g.onload(() => { g.script('scripts/lib/text_editor.min.js', init); }); }()); | > > > > > > > > > > | 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 | }); } window.setTimeout(() => { if ((v = localStorage.getItem(backup_key)) && v.trim() !== t.textarea.value.trim() && window.confirm(msg_restore)) { t.textarea.value = v; } else { localStorage.removeItem(backup_key); } }, 50); window.setInterval(() => { if (t.textarea.value.trim() === t.textarea.defaultValue.trim()) { return; } var v = localStorage.getItem(backup_key); if (v && v.trim() === t.textarea.value.trim()) { return; } localStorage.setItem(backup_key, t.textarea.value); console.log('Saved'); }, 10000); } g.onload(() => { g.script('scripts/lib/text_editor.min.js', init); }); }()); |
Modified src/www/admin/static/scripts/web_gallery.js from [98ae149376] to [bc4b4561f0].
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 27 28 29 30 | a.setAttribute('data-pos', i); a.onclick = function (e) { e.preventDefault(); openImageBrowser(items, this.getAttribute('data-pos')); return false; }; } }; window.enableImageGallery = enableGallery; function openImageBrowser(items, pos) { div = document.createElement('div'); | > > | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | a.setAttribute('data-pos', i); a.onclick = function (e) { e.preventDefault(); openImageBrowser(items, this.getAttribute('data-pos')); return false; }; } document.querySelectorAll('div.slideshow').forEach(e => enableSlideshow(e)); }; window.enableImageGallery = enableGallery; function openImageBrowser(items, pos) { div = document.createElement('div'); |
︙ | ︙ | |||
78 79 80 81 82 83 84 85 | img.style.width = 0; img.style.height = 0; img.src = items[pos].href; img.pos = pos; } }()); | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | img.style.width = 0; img.style.height = 0; img.src = items[pos].href; img.pos = pos; } function enableSlideshow(gallery) { var images = gallery.getElementsByTagName('figure'); var count = images.length; var div = document.createElement('div'); div.className = 'index'; for (var i = 0; i < count; i++) { var btn = document.createElement('button'); if (i == 0) { btn.className = 'current'; } btn.onclick = (e) => { var btn = e.target; var i = parseInt(btn.innerText, 10)-1; gallery.firstChild.scrollTop = i*400; gallery.querySelector('.current').classList.remove('current'); btn.classList.add('current'); }; btn.innerText = i + 1; div.appendChild(btn); } gallery.appendChild(div); var nav = document.createElement('div'); nav.className = 'nav'; var get_current_idx = () => parseInt(gallery.querySelector('.current').innerText, 10)-1; var btn = document.createElement('button'); btn.className = 'prev'; btn.onclick = () => { var i = get_current_idx() - 1; var buttons = gallery.querySelectorAll('.index button'); if (i < 0) { i = buttons.length - 1; } buttons[i].click(); }; btn.innerHTML = '◀'; nav.appendChild(btn); var btn = document.createElement('button'); btn.className = 'prev'; btn.onclick = () => { var i = get_current_idx()+1; var buttons = gallery.querySelectorAll('.index button'); if (i >= buttons.length) { i = 0; } buttons[i].click(); }; btn.innerHTML = '▶' nav.appendChild(btn); gallery.appendChild(nav); } }()); |
Modified src/www/admin/static/styles/01-layout.css from [c3e802ffb9] to [830e9e0404].
︙ | ︙ | |||
25 26 27 28 29 30 31 | --gBorderColor: #999; --gLightBorderColor: #333; --gLightBackgroundColor: #222; --gLinkColor: #99f; --gHoverLinkColor: 250, 127, 127; } | | | 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | --gBorderColor: #999; --gLightBorderColor: #333; --gLightBackgroundColor: #222; --gLinkColor: #99f; --gHoverLinkColor: 250, 127, 127; } html.dark .header .menu, html.dark .header .menu a, html.dark .header .menu li.current h3 span[data-icon]::before, html.dark nav.tabs .current a { color: rgb(var(--gTextColor)) !important; text-shadow: 0px 0px 5px rgb(var(--gBgColor)) !important; } html { width: 100%; height: 100%; |
︙ | ︙ |
Modified src/www/admin/static/styles/03-forms.css from [4b6d641eb1] to [21f88f170c].
︙ | ︙ | |||
89 90 91 92 93 94 95 96 97 98 | input[type=password], input[type=range], input[type=search], input[type=tel], textarea, select, .input-list, .file-selector { padding: .4rem .6rem; font-family: inherit; min-width: 20em; max-width: 100%; border: 1px solid rgb(var(--gMainColor)); font-size: inherit; background: rgb(var(--gBgColor)); color: rgb(var(--gTextColor)); | > < | 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | input[type=password], input[type=range], input[type=search], input[type=tel], textarea, select, .input-list, .file-selector { padding: .4rem .6rem; font-family: inherit; min-width: 20em; max-width: 100%; border: 1px solid rgb(var(--gMainColor)); border-radius: .25rem; font-size: inherit; background: rgb(var(--gBgColor)); color: rgb(var(--gTextColor)); transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out; } textarea.full-width, input.full-width { width: calc(100% - 1.2rem); } |
︙ | ︙ |
Modified src/www/admin/static/styles/04-dialogs.css from [e22f8ea0ea] to [51685f7e90].
︙ | ︙ | |||
52 53 54 55 56 57 58 | } #dialog.loaded > img, #dialog.loaded > audio, #dialog.loaded > video { opacity: 1; height: initial; } | | > > > > > | 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | } #dialog.loaded > img, #dialog.loaded > audio, #dialog.loaded > video { opacity: 1; height: initial; } #dialog > iframe, #dialog > div { border-radius: .5em; box-shadow: 0px 0px 5px #000; background: #fff; } #dialog > div { padding: 1em; } #dialog.loaded > iframe { width: 90%; opacity: 1; } |
︙ | ︙ |
Modified src/www/admin/static/styles/05-navigation.css from [088a5c1c37] to [c15fe949c0].
︙ | ︙ | |||
181 182 183 184 185 186 187 | margin: 0 auto 0 auto; padding: .1em; background: rgba(var(--gSecondColor), .5); margin-bottom: 5px; text-shadow: none; } | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 | margin: 0 auto 0 auto; padding: .1em; background: rgba(var(--gSecondColor), .5); margin-bottom: 5px; text-shadow: none; } /** * Dropdown, eg. interactive <select> */ nav.dropdown { position: relative; height: 2.3rem; border: 1px solid rgb(var(--gMainColor)); border-radius: .25rem; } nav.dropdown ul { background-color: rgb(var(--gBgColor)); top: 0; left: 0; right: 0; position: absolute; border-radius: .5em; } nav.dropdown ul::after { position: absolute; right: 0; top: 0; display: flex; align-items: center; justify-content: center; width: 1.5em; height: 1.5em; font-size: 1.5em; content: "↓"; font-family: "gicon"; } nav.dropdown li { display: none; } nav.dropdown li.selected, nav.dropdown:hover li { display: block; } nav.dropdown li a { display: flex; align-items: center; justify-content: space-between; padding: .4rem .6rem; text-decoration: none; height: 1.5em; color: unset; } nav.dropdown li a strong { font-weight: normal; } nav.dropdown li a small { color: var(--gBorderColor); margin-right: 2em; } nav.dropdown li:nth-child(1) { display: none; } nav.dropdown:hover { border-color: transparent; } nav.dropdown:hover li:nth-child(even) a { background: rgba(var(--gSecondColor), 0.2); } nav.dropdown:hover ul { box-shadow: 0 0 5px .2rem rgba(var(--gMainColor), 0.5); border-radius: .25rem; } nav.dropdown:hover .selected a { box-shadow: 0 0 5px .2rem rgba(var(--gMainColor), 0.5); color: rgb(var(--gHoverLinkColor)); } nav.dropdown:hover li a strong { font-weight: bold; } nav.dropdown:hover li a:hover { background: rgba(var(--gMainColor), 0.2); color: rgb(var(--gHoverLinkColor)); } @media handheld, screen and (max-width:981px) { nav.dropdown:hover li:nth-child(1) { display: block; } } |
Modified src/www/admin/static/styles/07-tables.css from [f6bdd9860c] to [f51df0a09c].
︙ | ︙ |
Modified src/www/admin/users/index.php from [1e8cb64c32] to [f68afbfad0].
︙ | ︙ | |||
11 12 13 14 15 16 17 | if ($format = qg('export')) { Session::getInstance()->requireAccess($session::SECTION_USERS, $session::ACCESS_ADMIN); Users::exportCategory($format, $current_cat); return; } | < < < | < < < < | < | | 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 | if ($format = qg('export')) { Session::getInstance()->requireAccess($session::SECTION_USERS, $session::ACCESS_ADMIN); Users::exportCategory($format, $current_cat); return; } $is_manager = $session->canAccess($session::SECTION_USERS, $session::ACCESS_WRITE); $categories = Categories::listAssocWithStats($is_manager ? null : Categories::WITHOUT_HIDDEN); // Deny access to hidden categories to users that are not admins if (!array_key_exists($current_cat, $categories)) { $current_cat = null; } $can_edit = $session->canAccess($session::SECTION_USERS, $session::ACCESS_ADMIN); $list = Users::listByCategory($current_cat); $list->loadFromQueryString(); if (!$current_cat) { $title = 'Liste des membres'; } elseif ($current_cat == -1) { $title = 'Tous les membres'; } else { $title = sprintf('Liste des membres — %s', $categories[$current_cat]->label ?? ''); } $tpl->assign(compact('can_edit', 'list', 'current_cat', 'categories', 'title')); $tpl->display('users/index.tpl'); |