MediCoder

Le blog de développeurs de Mediboard. Attention : jargon technique et barbarismes lexicaux probables, yeux sensibles s'abstenir...
Dernière publication 23/05/2014 12h08
(17 Articles | 24306 Visites | Activité=2.00)

Trouver :
Par mytto20873 points  le 23/05/2014 12h08

Voici un fragment de code montrant une sélection des interventions susceptibles d'être en USCPO à une date donnée, par défaut une date courante.

// $date est entre $date est entre $operation.date + $operation.uscpo (days) 
$where[1] = "operations.date <= '$date'";
$where[2] = "DATE_ADD(operations.date, INTERVAL duree_uscpo DAY) > '$date'";

La seconde clause ne permet pas d'utiliser d'index puisque la valeur à tester est calculatoire. La première clause donnera donc un usage de l'index sur la date de l'interventon inférieur à une date donnée, a priori tout va bien. A priori seulement, car la date en question est presque toujours une date récente, donc l'index ne filtrera que les opérations dans le futur, lesquelles sont rapidement très minoritaires à mesure de l'usage du système.

L'index est ainsi rendu — presque — inutile, la requête réalise un — presque — full scan sur la table des interventions.

Pour le rendre à nouveau efficace il faudrait limiter la recherche sur une petite portion des opérations. A bien regarder l'usage du champ duree_uscpo, on se rend compte que les valeurs ne varient que de 0 à 5. En prenant 10 comme majorant, si operation.date + operations.duree_uscpo > $date, on est certain que operation.date + 10 > $date.

On peut ainsi ajouter une contrainte en apparence inutile en termes de filtre final, mais extrêmement utile pour le filtre initial réalisé par l'index:

// Minimal date will narrow scope and boost index execution greatly
$date_min = CMbDT::date("-10 DAY", $date);
$where[1] = "operations.date BETWEEN '$date_min' AND '$date'";
$where[2] = "DATE_ADD(operations.date, INTERVAL duree_uscpo DAY) > '$date'";

Dès lors l'index peut immédiatement se borner à scanner 10 jours d'interventions sur potentiellement des années d'historiques, rendant la requête quasi immédiate. Sur une base de 3 ans (1000 jours), la requête est environ 100 fois plus rapide.

Certes on prend un risque en mettant cette valeur en dur dans le code, au cas ou la durée USCPO serait anormalement saisie à plus de 10 jours, ce qui serait une aberration métier. On peut alors contraindre le champ avec une valeur maximale, et se servir de cette contrainte dans notre clause :

$operation = new COperation();
$max_uscpo = $operation->_specs["duree_uscpo"]->max;
 
// Minimal date will narrow scope and boost index execution greatly
$date_min = CMbDT::date("-$max_uscpo DAY", $date);
$where[1] = "operations.date BETWEEN '$date_min' AND '$date'";
$where[2] = "DATE_ADD(operations.date, INTERVAL duree_uscpo DAY) > '$date'";

Pour une fois une contrainte métier est au service de la réalisation technique.

Par mytto20873 points  le 10/05/2013 13h48

Il es fréquent d'activer des traitements de manière périodique en utilisant des tâches planifiées sur le serveur d'application de Mediboard. L'usage de l'ordonnanceur cron et de ces cron tables rend l'affaire assez aisée grâce à une syntaxe claire et compacte.

Par exemple, si on souhaite executer une action d'un module toutes les minutes, il suffit d'ajouter la ligne suivante à la table

* * * * * http://mediboard/?login=user:pass&m=module&a=action

Le problème se corse sérieusement quand on souhaite interrompre ce traitement une plage horaire donnée, par exemple quand une ressource nécessaire n'est pas disponible pour une raison x ou y. En effet, si veut exécuter une action toutes les minutes sauf de 06h40 à 07h11 par exemple, il faut ruser et découper les instructions de la table de la manière suivante:

*	0-5,8-23 * * * http://mediboard/?login=user:pass&m=module&a=action
0-39	6	 * * * http://mediboard/?login=user:pass&m=module&a=action
12-59	7	 * * * http://mediboard/?login=user:pass&m=module&a=action

Autant dire quelque chose d'assez pénible, totalement imbuvable et potentiellement source d'erreur si les planifications ont le malheur de se chevaucher.

Pour résoudre ce problème, Mediboard permet désormais de fournir des exceptions horaires pendant lesquelles les actions, vues et controlleurs sont désactivés, grâce au paramètres muters :

* * * * * http://mediboard/?login=user:pass&m=module=a=action&muters=06:40-07:11

Le paramètre s'exprime sous la forme d'une collection de paires min-max d'horaires au format ISO, complets ou non. Par exemple:

muters=11-12                # de 11h à 12h
muters=06:41-07:13:46       # de 06h41 à 07h13m46
muters=11-12-06:41-07:13:46 # sur les deux périodes précédentes

Par mytto20873 points  le 02/02/2013 22h08

Revenons aujourd'hui sur de l'optimisation côté serveur.

Certaines fonctions utilitaires — ou méthodes statiques — sont appelées de manière très régulières avec des arguments identiques ou presque au sein d'une même requete HTTP. Si ces fonctions sont couteuses en ressources, par exemple en exécutant des requêtes SQL? sur un serveur tiers, on peut avoir intérêt à créer un cache d'API intra-requête qui, par définition, conservera en mémoire les valeurs calculées pour un jeu d'arguments donné, afin de les ressortir sans calcul en cas d'appel ultérieur avec le même jeu.

Remarque: il faut bien différencier les caches intra-requête qui peuvent se baser sur un stockage temporaire, souvent dans un variable static, des caches inter-requêtes qui nécessitent l'usage de la mémoire partagée et qui ont nécessité d'être vidés de manière explicite.

Pour la suite, nous prendrons l'exemple de la fonction CSejour::getTagNDA() qui consomme pas mal, notamment en réalisant 4 requêtes SQL — certes mises en cache coté serveur — qui induisent une latence importante sur les gros volumes d'usage.

static function getTagNDA($group_id = null, $type_tag = "tag_dossier") {
  // Calcul compliqué de $tag
  // ...
  // ...
 
  return $tag;
}

Implémentation manuelle

La technique basique consiste à déclarer un tableau statique au sein de la fonction, à calculer une clé de hashage des paramètres et à stocker la valeur calculée en fin de premer traitement pour la ressortir en début des passages suivants.

static function getTagNDA($group_id = null, $type_tag = "tag_dossier") {
  static $cache = array();
  $cache_id = "$group_id-$type_tag";
  if (isset($cache[$cache_id])) {
   return $cache[$cache_id];
  }
 
  // Calcul compliqué de $tag
  // ...
  // ...
 
  return $cache[$cache_id] = $tag;
}

La technique est bonne, efficace et fiable, demande 5 lignes en début de fonction et une altération de la ligne finale du return.

En termes de propagation vers d'autres fonctions, la seule chose qui change vraiment et la clé $cache_id qui concatène l'ensemble des paramètres d'appel. On souhaiterait factoriser de fragment et rendre la déclaration du cache de fonction plus explicite.

Implémentation automagique

Voici une classe utilitaire CFunctionCache (external link) qui se charge de du stockage des valeurs et de leur contexte.

Modification : pour des raisons de performances et de compatibilité, le contexte est désormais passé explicitement (cf. commentaires).

static function getTagNDA($group_id = null, $type_tag = "tag_dossier") {
  $context = array(__METHOD__, func_get_args());
  if (CFunctionCache::exist($context)) {
    return CFunctionCache::get($context);
  }
 
  // Calcul compliqué de $tag
  // ...
  // ...
 
  return CFunctionCache::set($context, $tag);
}

Accessoirement, en bonus, les propriétés CFunctionCache:$data et CFunctionCache:$hits donnent des infos sur respectivement l'état du cache pour l'ensemble de fonctions mises en cache et sur le nombre d'appels réussis au cache.

Par mytto20873 points  le 17/10/2012 09h02

Voici un anodin fragment javascript qui ne parait pas affreux à prime abord. Et pourtant, on peut y voir un code à moderniser. Explications.

function submitImeds(oForm){
  submitFormAjax(oForm, 'systemMsg', { onComplete : function() { reloadUserImeds(oForm.user_id.value, oForm.tag.value, oForm.type.value) } });
}
 
function reloadUserImeds(user_id, tag, type){
  var url = new Url;
  url.setModuleAction("dPImeds", "httpreq_vw_id_imeds");
  url.addParam("user_id", user_id);
  url.addParam("tag"    , tag);
  url.addParam("type"   , type);
  url.requestUpdate('user-imeds');
}

Regrouper dans un objet utilitaire

On voit bien que les deux fonctions — presque — jusque dans leur noms, forment un tout homogène et indissociable autour de UserImeds, autant le formaliser par un objet qui fournira entre autre un namespace. On remarquera qu'il s'agit d'un couple onSubmit/reload ultra classique...

function submitImeds(...){
  ...
}
 
function reloadUserImeds(...){
  ...
}

Devient alors :

UserImeds = {
  onSubmit: function(...) {  
    ...
  },
 
  reload: function(...) {
    ...
  }
}

Passage de paramètres simplifié

On voir apparaitre un passage de paramètres étonnant: tous sont issus directement du formulaire.

reloadUserImeds(oForm.object_id.value, oForm.tag.value, oForm.type.value)

Pourquoi ne pas tout simplement passer le formulaire ? Sans compter que si le fomulaire change de contenu, on voudra très probablement adapter son usage dans la fonction reload().

UserImeds.reload(form)

onSubmit et son callback

Voici une bien lourde facon d'utiliser l'ancien submitFormAjax(), certes cette fonction aurait du être supprimée depuis longtemps pour forcer la migration vers son remplaçant submitFormAjax().

function submitImeds(oForm){
  submitFormAjax(oForm, 'systemMsg', { onComplete : function() { reloadUserImeds(oForm.object_id.value, oForm.tag.value, oForm.type.value) } });
}

On notera :

  • la simplification syntaxique de oForm qui devient form car on ne souhaite pas faire semblant de typer du javascript pour au final n'apporter qu'une très maigre information
  • la disparition du paramètre 'systemMsg' car on souhaite que tous les formulaires soumis en ajax le soient vers la messagerie système.
  • la simplification du fait que truc est un raccourci — ouf! — pour l'indigeste et quasi systématique { onComplete: truc() } dans les fonctions ajax, raccourci déjà abordé précédemment
  • la simplification par curry qui instancie une fonction de sorte que truc.curry(bidule) soit un élégant équivalent de function() { truc(bidule) }

onSubmit: function(form) {  
  return onSubmitFormAjax(form, UserImeds.reload.curry(form));
}

Meilleur usage de URL

Enfin on pourra simplier légèrement la forge de la requête par -+URL+.

function reloadUserImeds(user_id, tag, type){
  var url = new Url;
  url.setModuleAction("dPImeds", "httpreq_vw_id_imeds");
  url.addParam("user_id", user_id);
  url.addParam("tag"    , tag);
  url.addParam("type"   , type);
  url.requestUpdate('user-imeds');
}

Notamment grace à :

  • addElement qui termine de justifier l'intérêt du passage en paramètre du formulaire seul
  • la construction qui inclue le quasi systématique setModuleAction depuis fort longtemps

reload: function(form) {
  var url = new Url('Imeds', 'httpreq_vw_id_imeds');
  url.addElement(form.user_id);
  url.addElement(form.tag    );
  url.addElement(form.type   );
  url.requestUpdate('user-imeds');
}

Au total

Nettement plus propre :

Imeds = {
  onSubmit: function(form) {  
    onSubmitFormAjax(form, UserImeds.reload.curry(form));
  },
 
  reload: function(form) {
    var url = new Url('Imeds', 'httpreq_vw_id_imeds');
    url.addElement(form.user_id);
    url.addElement(form.tag    );
    url.addElement(form.type   );
    url.requestUpdate('user-imeds');
  }
}

Par mytto20873 points  le 04/06/2012 21h49

Voici un exemple typique de formulaire qui, correct en son temps et toujours opérationnel, n'a pas suivi les améliorations du framework, la propagation étant la face B laborieuse du refactoring? permanent de Mediboard.

<form name="editPrestFrm{{$_sejour->sejour_id}}" method="post">
  <input type="hidden" name="m"          value="dPplanningOp" />
  <input type="hidden" name="dosql"      value="do_sejour_aed" />
  <input type="hidden" name="sejour_id"  value="{{$_sejour->sejour_id}}" />
  <input type="hidden" name="patient_id" value="{{$_sejour->patient_id}}" />
  <select name="prestation_id" onchange="submitFormAjax(this.form, 'systemMsg')">
    <option value="">— Prestation</option>
    {{foreach from=$prestations item=_prestation}}
    <option value="{{$_prestation->_id}}" 
      {{if $_sejour->prestation_id == $_prestation->_id}} selected = selected {{/if}}
    >
      {{$_prestation->_view}}
    </option>
    {{/foreach}}
  </select>
</form>

Essayons de tout analyser, morceau par morceau

Entête

<form name="editPrestFrm{{$_sejour->sejour_id}}" method="post">

  • le nom n'est pas merveilleux, le suffixe Frm est un brin inutile, il s'agit bien d'un formulaire...
  • La clé de sejour_id l'objet est explicite, on aurait préfèrait un _id voir un _guid (solution retenue
  • le onsubmit n'est pas intercepté, ce qui signifie aucun contrôle côté client et une soumission Ajax gérée par les champs : c'est mal!

Résultat :

<form name="Prestations-{{$_sejour->_guid}}" method="post" onsubmit="return onSubmitFormAjax(this);">

Contrôleur

<input type="hidden" name="m"     value="dPplanningOp" />
<input type="hidden" name="dosql" value="do_sejour_aed" />

Le contrôleur est explicite est cible un script spécifique, ce qui n'est pas nécessaire dans l'immense majorité des cas, en paticulier ici. On passera donc la classe pour aiguiller sans effort.

{{mb_class object=$_sejour}}

Clé

<input type="hidden" name="sejour_id"  value="{{$_sejour->sejour_id}}" />
<input type="hidden" name="patient_id" value="{{$_sejour->patient_id}}" />

  • La clé est passée de manière inutilement explicite, et n'utilise pas le helper dédié mb_key.
  • la clé étrangère vers le patient est ici totalement inutile

{{mb_key object=$_sejour}}

Sélection

C'est ici le champ qui justifie le formulaire, choisir une clé étrangère parmi une collection, en l'occurrence associer une prestation à un séjour.

<select name="prestation_id" onchange="submitFormAjax(this.form, 'systemMsg')">
    <option value="">— Prestation</option>
    {{foreach from=$prestations item=_prestation}}
    <option value="{{$_prestation->_id}}" 
      {{if $_sejour->prestation_id == $_prestation->_id}} selected = selected {{/if}}
    >
      {{$_prestation->_view}}
    </option>
    {{/foreach}}
  </select>

On utilise ici un select que l'on créé de toutes pièces avec de nombreux défauts :

  • pas de classes, donc pas de contrôle côté client
  • traitement de la soumission (ancienne version en plus ! ) dans l'évènement onchange en lieu et place d'un simple déclenchement
  • foreach totalement standard et répétitif, géré par le mb_field des propriétés? de type référence, en passant une collection d'objets

{{mb_field object=$_sejour field=prestation_id choose=CPrestation options=$prestations onchange="this.form.onsubmit();"}}

Au total

Pour résumer ce long discours :

<form name="Prestations-{{$_sejour->_guid}}" method="post" onsubmit="return onSubmitFormAjax(this);">
  {{mb_class object=$_sejour}}
  {{mb_key   object=$_sejour}}
  {{mb_field object=$_sejour field=prestation_id choose=CPrestation options=$prestations onchange="this.form.onsubmit();"}}
</form>

A défaut de propager avec une voracité infatigable, commençons par ne plus en créer de vilains formulaires !

Par rhum17461 points  le 31/05/2012 15h48

Les calculs sur les dates peuvent être de vrais casse-têtes (premier date du mois prochain, dernier jour du mois courant, ...). L'utilisation du langage naturel de strtotime(), la fonction sollicitée par nos mbDate()-like, permet de faciliter tout ça !

Exemple d'un code avant (long, difficile à comprendre et buggé pour certaines date spécifiques) :

$date = CValue::getOrSession("date", mbDate());
$first_day_curr_month = mbTransformTime("+ 0 month", $date, "%Y-%m-01");
$last_day_curr_month  = mbTransformTime("+ 1 month", $month_min, "%Y-%m-01");
$last_day_curr_month  = mbDate("- 1 day", $month_max);
$last_day_last_month  = mbDate("-1 month", $date);
$first_day_next_month = mbDate("+1 month", $date);

Après utilisation du langage naturel :

$date = CValue::getOrSession("date", mbDate());
$first_day_curr_month = mbDate("first day of +0 month", $date);
$last_day_curr_month  = mbDate("last day  of +0 month", $date);
$last_day_last_month  = mbDate("last day  of -1 month", $date);
$first_day_next_month = mbDate("first day of +1 month", $date);

Vous pouvez utiliser beaucoup de notions en langage naturel :

  • des unitées de temps (year, month, week, day, hour, minute, second, am, pm, fortnight)
  • des mots courants (ago, now, last, this, next, tomorrow, yesterday)
  • des opérateurs (- et +)
  • des nombres (1, 2, 3, …)
  • des times zones (gmt, pdt, akst)
  • des noms de mois et leurs abréviations
  • des noms de jours de la semaine et leurs abréviations

Référence : la fonction strtotime sur le site php.net (external link)

Par mytto20873 points  le 30/05/2012 20h13

La tyrannie des URL à rallonge de Mediboard est enfin terminée !

Sus aux paramètres GET qui piquent les yeux !

Longue vie aux nouveaux raccourcis !

Plutôt qu'un long discours, voici un exemple très significatif :

http://localhost/index.php?m=dPpatients&a=httpreq_vw_patient&ajax=1&suppressHeaders=1
http://localhost/?m=patients&ajax=httpreq_vw_patient

Comme vous l'avez compris, ces deux URL sont strictement équivalents :

index.php
il est inutile, chacun le sait, mais ça va mieux en le disant ;
dPpatient => patient
on peut omettre le dP, le dispatcher se chargera de retrouver le module correspondant ;
a=view&ajax=1 => ajax=view
le mode peut directement être invoqué comme clé de la vue à afficher (!) ;
suppressHeaders=1
désormais implicite dans le cas du mode ajax (et wsdl).

Quelques autres exemple d'URL équivalents :

http://localhost/?m=system&a=about&dialog=1
http://localhost/?m=system&dialog=about

http://localhost/?m=ecap&a=soap_server&wsdl
http://localhost/?m=ecap&wsdl=soap_server

Au final, ce sont 5 modes qui sont à ce jour gérés par l'URL dispatcher : tab, a, dialog, ajax, wsdl.

Vieille idée: Ce refactoring est une idée assez ancienne suggérée en 2009 par Fabien : item clôturé donc!
Par mytto20873 points  le 06/05/2012 15h01

Voici un beau fragment de code regroupant trois séries d'assignations qui mériteraient d'être refactorées, dans la double optique d'être plus compact et lisible, afin d'en faire ressortir la logique plus clairement.

function updateFormFields() {
  parent::updateFormFields();
 
  $this->codes_ccam = strtoupper($this->codes_ccam);
  if ($this->codes_ccam) {
    $this->_codes_ccam = explode("|", $this->codes_ccam);
  } 
  else {
    $this->_codes_ccam = array();
  }
 
  $this->_hour_op = substr($this->temp_operation, 0, 2));
  $this->_min_op  = substr($this->temp_operation, 3, 2));
 
  if ($this->libelle_sejour) {
    $this->_view = $this->libelle_sejour;
  } 
  elseif ($this->libelle) {
    $this->_view = $this->libelle;
  } 
  else {
    $this->_view = $this->codes_ccam;
  }
}

Opérateur ternaire

Un cas typique d'opérateur ternaire manquant :

$this->codes_ccam = strtoupper($this->codes_ccam);
if ($this->codes_ccam) {
  $this->_codes_ccam = explode("|", $this->codes_ccam);
} 
else {
  $this->_codes_ccam = array();
}

Au delà de la syntaxe qui est plus compacte, cela permet de mettre en évidence que la condition n'a pour but que de faire varier l'affectation, et non de créer une branche cyclomatique nouvelle, avec tout un — possible — tas d'opérations impératives.

$this->_codes_ccam = this->codes_ccam ? 
  explode("|", strtoupper($this->codes_ccam)) : 
  array();

list + explode vs. substr

Ici on veut séparer les composantes HH et MM d'une variable de type ISO TIME HH:MM.

$this->_hour_op = substr($this->temp_operation, 0, 2));
$this->_min_op  = substr($this->temp_operation, 3, 2));

Si la syntaxe de substr() est très puissante et son exécution rapide, utiliser explode() permet de mieux mettre en évidence la séparation des composantes, tout en étant plus compact.

En outre, la syntaxe est beaucoup plus robuste si par malheur les composantes n'ont qu'un chiffre, ce qui arrive potentiellement lorsque la valeur ne vient pas de directement des données de Mediboard, par exemple 8:20 au lieu de 08:20. Ce comportement pad proof est celui de la plupart des systèmes compatibles avec ISO 8601 (external link), comme par exemple MySQL ou PHP

list($this->_hour_op, $this->_min_op) = explode(":", $this->temp_operation);

Valeurs par défaut en série

Enfin, ce dernier fragment montre un cas typique de valeurs par défaut en série, ou valeurs prioritaires, que l'on cherche pour la première d'entre elles qui n'évalue pas à false — cad null ou la chaine vide dans la plupart de cas.

if ($this->libelle_sejour) {
  $this->_view = $this->libelle_sejour;
} 
elseif ($this->libelle) {
  $this->_view = $this->libelle;
} 
else {
  $this->_view = $this->codes_ccam;
}

On aurait déjà pu compacter le code en utilisant un double opérateur ternaire, seulement la lisibilité n'est pas merveilleuse, surtout lorsqu'on a de nombreuses valeurs dans la série. Par ailleurs ce genre de logique est très fréquemment utilisée dans Mediboard, ainsi le framework fournit un opérateur qui permet de retourner la première valeur !false d'une série variatique.

$this->_view = CValue::first($this->libelle_sejour, $this->libelle, $this->codes_ccam);

Au total

Au total nous avons un code plus concis et plus clair donc plus maintenable.

function updateFormFields() {
  parent::updateFormFields();
 
  $this->_codes_ccam = this->codes_ccam ? 
    explode("|", strtoupper($this->codes_ccam)) : 
    array();
 
  list($this->_hour_op, $this->_min_op) = explode(":", $this->temp_operation);
 
  $this->_view = CValue::first($this->libelle_sejour, $this->libelle, $this->codes_ccam);
}

Nous avons vu également qu'il était légèrement plus robuste !

Par mytto20873 points  le 16/04/2012 09h33

Voici un cas typique de code à simplifier. On souhaite ici avoir un affichage qui dépend du $vue, valant classique ou compacte, avant dans les deux cas, une base commune.

Le snippet côté template :

<div class="wrapper_line{{if $vue == 'compacte'}}_compacte{{/if}}">
...
</div>

Le code CSS concerné :

div.wrapper_line {
  position: relative;
  height: 2em;
  margin-bottom: 8px;
}
 
div.wrapper_line_compacte {
  position: relative;
  height: 1.1em;
  margin-bottom: 8px;
}

Plusieurs remarques :

  1. les deux classes CSS sont très proches en contenu
  2. le code n'est pas symétrique, car il n'y a pas de style spécifique pour la vue classique: elle se définie comme "non-compacte"
  3. la condition Smarty qui concatène un mot dans la classe n'est pas très élégante

Traitons les deux premiers point en factorisant les styles avec des classes multiples. Cela évitera de propager des modifications sur les deux modes, ouvrira la voie à d'autre modes et rendra le différentiel plus explicite :

div.wrapper_line {
  position: relative;
  margin-bottom: 8px;
}
 
div.wrapper_line.classique {
  height: 2em;
}
 
div.wrapper_line.compacte {
  height: 1.1em;
}

Le troisième point est traité de facto par cette nouvelle structure :

<div class="wrapper_line {{$vue}}">
...
</div>

Par mytto20873 points  le 29/03/2012 15h36

On a régulièrement besoin d'enregistrer une valeur donnée sur une collection d'objets.

Prenons l'exemple d'une vue qui permet d'annuler des séjours. On construira classiquement un tableau avec un bouton d'annulation à la fin de chaque ligne :

{{foreach from=$sejours item=_sejour}}
... 
 
<form name="Edit-{{$_sejour->_guid}}" action="?" method="post">
 
<input type="hidden" name="m" value="planningOp" />
<input type="hidden" name="dosql" value="do_sejour_aed" />
<input type="hidden" name="sejour_id" value="{{$_sejour->_id}}" />
 
<input type="hidden" name="annule" value="1" />
 
...
 
</form>
 
...
{{/foreach}}

Seulement on aimerait également avoir un bouton pour tout annuler, sans cliquer sur n boutons et encore moins en chainant des requêtes Ajax.

Voici comment transformer un do_aed classique : il suffit d'ajouter un champ dont le nom est celui de la clé avec un s final, et donc la valeur est une sérialisation des identifiants.

<form name="Edit-sejours" action="?" method="post">
 
<input type="hidden" name="m" value="planningOp" />
<input type="hidden" name="dosql" value="do_sejour_aed" />
<input type="hidden" name="sejour_ids" value="{{$sejours|@array_keys|@join:"-"}}" />
 
<input type="hidden" name="annule" value="1" />
 
</form>

On pourra également laissé la valeur en blanc pour la remplir en Javascript, par exemple sur l'état de cases à cocher :

<input type="hidden" name="sejour_ids" value="" />
{{foreach from=$sejours item=_sejour}}
<input class="sejour" type="checkbox" value="{{$_sejour->_id}}" />
{{/foreach}}

et

var sejour_ids = $$('input.sejour:checked').pluck('value');
$V(this.form.sejour_ids, sejour_ids);

Le tout, bien entendu, sans avoir à coder de do_multiple_aed.php spécifique.

Remarque: Les controlleurs multiples fonctionnent également en suppression.
Page: 1/2 Next Page
1 2

Sponsors privilégiés

Mediboard project