Bonjour à tous.

Voilà le contexte. J'utilise Redmine 3.3.3, basé sur Ruby On Rails.
Afin de réaliser des cartes et de gérer des issues géoréférencée, un table "pointes" permet pour chaque Issue d'être géolocalisée.
Cette table contient l'id de l'Issue, l'id du pointé, un point (base de données spatiale postgresql), un état et diverses informations.
Afin de visualiser ces coordonnées mais aussi de pouvoir en modifier les informations, plusieurs plugins ont été développés (permettant d'ajouter / supprimer un plusieurs pointés par FT via des requêtes rest).
Pour afficher les informations des pointés, un fichier a été ajouté dans <nom du plugin>/lib/<nom du plugin>/view_hook_listeners.rb
Ceci va exécuter le fichier app/views/<nom du plugin>/issue/_issus_details.html.erb pour injecter du code dans la page de visualisation de l'Issue et ça marche très bien.

view_hook_listeners.rb :
module GeometryStatus
  # Voir : http://www.redmine.org/projects/redmine/wiki/Hooks
  class Hooks < Redmine::Hook::ViewListener
    # rewrite select for trackers on issue form // view_issues_show_details_bottom
    def view_issues_show_details_bottom(context={})
      issue = context[:issue]
      project = context[:project]

      --- du code non copié ici car un peu complexe ---
        # charger le fichier app\views\geometry_status\issues\_issue_details.html.erb
        return context[:controller].send(:render_to_string, partial: 'geometry_status/issues/issue_details', locals: { project: project, issue: issue, pointes: pointes, portee_du_defaut: portee_du_defaut, port: qgis_plugin_port?, chantier_name: chantier_name, geomaps_map_server_login: geomaps_map_server_login?, geomaps_map_server_password: geomaps_map_server_password?, geomaps_map_server_url: geomaps_map_server_url?, nb_status_total: nb_status_total, nb_status_to_validated: nb_status_to_validated, nb_status_resolved: nb_status_resolved, nb_status_refused: nb_status_refused })
      end
      ''
    end
  end
end

Ce que je veux

Le formulaire affichant les informations des pointés a été injecté en javascript dans la page de détails de l'Issue et fonctionne (la sauvegarde des modifications est faite en AJAX). Toutefois, afin d'obtenir une meilleur intégration dans Redmine j'ai souhaité non pas utiliser un formulaire normal avec form mais le faire en rails (sinon ça pose des problèmes de détection de champs modifiés non sauvé (ou sauvé) mal pris en charge par Rails puisqu'on ne passe pas par le framework) :
Le code suivant est placé dans une boucle, en effet il peut y avoir plusieurs instances de 'pointé' par FT, chaque instance ayant son formulaire propre.

        <%= form_for :pointe, html: {class: 'geometry_pointe_form'}  do |form| %>
          <%= form.hidden_field :id, value: pointe.id %>
                <legend><strong><%= l(:geometry_legend_status) %></strong></legend>
                <%= form.select(:statut, options_for_select([[l(:geometry_status_new), 1],
                                                            [l(:geometry_status_refused), 2],
                                                            [l(:geometry_status_accepted), 3],
                                                            [l(:geometry_status_to_validated), 4],
                                                            [l(:geometry_status_resolved), 5],
                                                            [l(:geometry_status_confirmed), 6]
                                                           ],
                                                           pointe.statut)) %>
               .... les autres  champs .....

          <%= form.submit "Submit" %>
        <% end %>

Ce que j'obtiens

J'obient exactement ce que je veux au niveau affichage, bien entendu le code est bien plus complexe.
Par contre impossible de savoir quoi mettre dans quel fichier pour capter le submit et sauver en base de données les informations modifiées.
J'obtiens "Page not found" systématiquement lorsque je clique sur le bouton submit.

J'ai bien tenté dans app/status/controller/pointe_controller.rb de mettre le code suivant mais sans aucun effet.
Et quand ça ne fonctionne pas, on peut pas dire de rails nous aide à comprendre les choses.

class PointeController < ApplicationController
  def create
    Pointes.create(params[:id])
  end

  def update
    @pointe = Pointes.find(params[:id]]))
    if @pointe.update(pointe_params)
      redirect_to 'pages/success'
    else
      redirect_to 'pages/error'
    end
  end

private
  def pointe_params
   params.require(:id).permit(:comment, :statut)
  end  
end

Voilà, j'ai passé deux jours à porter de façon propre le formulaire que j'avais fait en javascript exclusivement, mais maintenant il faut que je le sauve ce(s) sacré(s) formulaire(s) !

Merci pour vos réponses.

2 réponses


Hello,

Est-ce que tu sais si l'erreur que tu as vient de l'url pour le formulaire ?
Dans ce cas là, il faut peut-être voir à utiliser le champs url de form_for.

Sinon, peut-être que ton update est bien appelé, mais qu'il ne trouve pas les redirect que tu lui donnes (pages/success ou pages/error)

Feneck91
Auteur

Salut et merci pour ta réponse.
Actuellement, ça fonctionne mais c'est bof bof...
1> Dans l'update il n'y a pas de redirect et déjà c'est peut-être ça qui manque, mais je ne sais pas quoi mettre comme redirect comme page (attention je suis dans un plugin sous Redmine). En fait pour le moment, toutes les modification en base sont envoyée via rest dans des plugins écris en python (plugin QGis).
2> Comme il n'y a pas de redirect, on ne voit pas si la sauvegarde a fonctionnée ou pas, en fait ça n'a choqué personne puisqu'en rest je n'utilise que le code retour.
3> Même si la sauvegarde échoue, le formulaire est considéré comme sauvé par le framework car si on change de page, il n'y a plus d'avertissement comme quoi des informations ont changées, bizarre...

Actuellement j'appel un javascript qui utilise l'API rest et si ça fonctionne alors je laisse le traitement par défaut (qui va sauvegarder une seconde fois le formulaire), sinon je bloque le traitement par défaut du bouton et donc rails le considère comme non sauvé.

Mon controller (simplifié) :

class PointesController < ApplicationController
  unloadable

  include ActionController::Live
  include ActionController::RespondWith
  include ActionController::MimeResponds

  before_filter :find_current_user
  respond_to :xml, :json

  #
  # show pointe by id
  #
  def show
    @pointes = Pointes.find(params[:id])
    respond_with @pointes
  end

  #
  # create pointe
  #
  def create
    @pointe = Pointes.new(params[:pointes])
    if @pointe.save
      respond_with(@pointe, status: 201, location: nil)
    else
      respond_with(@pointe.errors, location: nil)
    end
  end

  #
  # update pointe
  #
  def update
    @pointe = Pointes.find(params[:id])
    if @pointe.update(params[:pointes])
      respond_with({:msg => "Pointé mis à jour"}, status: 201, location: nil)
    else
      respond_with({:msg => @pointe.errors}, location: nil)
    end
  end

  #
  # delete pointe
  #
  def delete
    Pointes.find(params[:id]).destroy
    respond_with({:msg => "Pointé supprimé"}, status: 201, location: nil)
  end

Le code du formulaire :

        <%= form_for :pointes, url: '../geometry/%d' % pointe.id, format: :json, remote: true, html: {class: 'geometry_pointe_form'}  do |form| %>
          <%= form.hidden_field :id, value: pointe.id %>
          <fieldset>
            <!-- Title -->
            <legend><strong><%= l(:pointe_num_id, :index => index + 1, :id=> pointe.id) %></strong></legend>
            <div class="geometry_pointe_column1">
              <br/>
              <% if pointe.pointe != nil && pointe.pointe.as_text().length != 0 %>
                <!-- WKT of the pointe -->
                <div class="bubble" id="pointe_geometry_copy_wkt_ok_<%= pointe.id %>" style="display:none;">
                  <div class="bubble-text">
                    <p><%= l(:text_copied_ok) %></p>
                   </div>
                </div>
                <div class="bubblenok" id="pointe_geometry_copy_wkt_nok_<%= pointe.id %>" style="display:none">
                  <div class="bubblenok-text">
                    <p><%= l(:text_copied_nok) %></p>
                   </div>
                </div>
                <button class="geometry_light_button" onClick="copy_clipboard_WKT(event, '<%= pointe.pointe.as_text() %>', 'pointe_geometry_copy_wkt_ok_<%= pointe.id %>', 'pointe_geometry_copy_wkt_nok_<%= pointe.id %>')" title="<%= l(:geometry_clic_button_copy_wkt_to_clipboard) %>">
                  <%= image_tag("copy_clipboard_wkt.png", :plugin => "geometry_status", :title => l(:geometry_clic_button_copy_wkt_to_clipboard), :align => "center") %>
                </button>
                <!-- Text of the pointe -->
                <div class="bubble" id="pointe_geometry_copy_ok_<%= pointe.id %>" style="display:none;">
                  <div class="bubble-text">
                    <p><%= l(:text_copied_ok) %></p>
                   </div>
                </div>
                <div class="bubblenok" id="pointe_geometry_copy_nok_<%= pointe.id %>" style="display:none">
                  <div class="bubblenok-text">
                    <p><%= l(:text_copied_nok) %></p>
                   </div>
                </div>
                <button class="geometry_light_button" onClick="copy_clipboard_WKT(event, '<%= pointe.pointe.x() %> <%= pointe.pointe.y() %>', 'pointe_geometry_copy_ok_<%= pointe.id %>', 'pointe_geometry_copy_nok_<%= pointe.id %>')" title="<%= l(:geometry_clic_button_copy_to_clipboard) %>">
                  <%= image_tag("copy_clipboard.png", :plugin => "geometry_status", :title => l(:geometry_clic_button_copy_to_clipboard), :align => "center") %>
                </button>
                <%= l(:geometry_pointe) %>
                <br/>
              <% end %>
              <% if pointe.point_attendu != nil && pointe.point_attendu.as_text().length != 0 %>
                <!-- WKT of the expected point -->
                <div class="bubble" id="expected_point_geometry_copy_wkt_ok_<%= pointe.id %>" style="display:none;">
                  <div class="bubble-text">
                    <p><%= l(:text_copied_ok) %></p>
                   </div>
                </div>
                <div class="bubblenok" id="expected_point_geometry_copy_wkt_nok_<%= pointe.id %>" style="display:none">
                  <div class="bubblenok-text">
                    <p><%= l(:text_copied_nok) %></p>
                   </div>
                </div>
                <button class="geometry_light_button" onClick="copy_clipboard_WKT(event, '<%= pointe.point_attendu.as_text() %>', 'expected_point_geometry_copy_wkt_ok_<%= pointe.id %>', 'expected_point_geometry_copy_wkt_nok_<%= pointe.id %>')" title="<%= l(:geometry_clic_button_copy_wkt_to_clipboard) %>">
                  <%= image_tag("copy_clipboard_wkt.png", :plugin => "geometry_status", :title => l(:geometry_clic_button_copy_wkt_to_clipboard), :align => "center") %>
                </button>
                <!-- Text of the expected point -->
                <div class="bubble" id="expected_point_geometry_copy_ok_<%= pointe.id %>" style="display:none;">
                  <div class="bubble-text">
                    <p><%= l(:text_copied_ok) %></p>
                   </div>
                </div>
                <div class="bubblenok" id="expected_point_geometry_copy_nok_<%= pointe.id %>" style="display:none">
                  <div class="bubblenok-text">
                    <p><%= l(:text_copied_nok) %></p>
                   </div>
                </div>
                <button class="geometry_light_button" onClick="copy_clipboard_WKT(event, '<%= pointe.point_attendu.x() %> <%= pointe.point_attendu.y() %>', 'expected_point_geometry_copy_ok_<%= pointe.id %>', 'expected_point_geometry_copy_nok_<%= pointe.id %>')" title="<%= l(:geometry_clic_button_copy_to_clipboard) %>">
                  <%= image_tag("copy_clipboard.png", :plugin => "geometry_status", :title => l(:geometry_clic_button_copy_to_clipboard), :align => "center") %>
                </button>
                <%= l(:geometry_expected_point) %>
                <br/>
              <% end %>
              <% if pointe.line_attendu != nil && pointe.line_attendu.as_text().length != 0 %>
                <!-- WKT of the expected line -->
                <div class="bubble" id="expected_line_geometry_copy_ok_<%= pointe.id %>" style="display:none;">
                  <div class="bubble-text">
                    <p><%= l(:text_copied_ok) %></p>
                   </div>
                </div>
                <div class="bubblenok" id="expected_line_geometry_copy_nok_<%= pointe.id %>" style="display:none">
                  <div class="bubblenok-text">
                    <p><%= l(:text_copied_nok) %></p>
                   </div>
                </div>
                <button class="geometry_light_button" onClick="copy_clipboard_WKT(event, '<%= pointe.line_attendu.as_text() %>', 'expected_line_geometry_copy_ok_<%= pointe.id %>', 'expected_line_geometry_copy_nok_<%= pointe.id %>')" title="<%= l(:geometry_clic_button_copy_wkt_to_clipboard) %>">
                  <%= image_tag("copy_clipboard_wkt.png", :plugin => "geometry_status", :title => l(:geometry_clic_button_copy_wkt_to_clipboard), :align => "center") %>
                </button>
                <%= l(:geometry_expected_line) %>
                <br/>
              <% end %>
              <% if pointe.polygon_attendu != nil && pointe.polygon_attendu.as_text().length != 0 %>
                <!-- WKT of the expected polygon -->
                <div class="bubble" id="expected_polygon_geometry_copy_ok_<%= pointe.id %>" style="display:none;">
                  <div class="bubble-text">
                    <p><%= l(:text_copied_ok) %></p>
                   </div>
                </div>
                <div class="bubblenok" id="expected_polygon_geometry_copy_nok_<%= pointe.id %>" style="display:none">
                  <div class="bubblenok-text">
                    <p><%= l(:text_copied_nok) %></p>
                   </div>
                </div>
                <button class="geometry_light_button" onClick="copy_clipboard_WKT(event, '<%= pointe.polygon_attendu.as_text() %>', 'expected_polygon_geometry_copy_ok_<%= pointe.id %>', 'expected_polygon_geometry_copy_nok_<%= pointe.id %>')" title="<%= l(:geometry_clic_button_copy_wkt_to_clipboard) %>">
                  <%= image_tag("copy_clipboard_wkt.png", :plugin => "geometry_status", :title => l(:geometry_clic_button_copy_wkt_to_clipboard), :align => "center") %>
                </button>
                <%= l(:geometry_expected_polygon) %>
                <br/>
              <% end %>
            </div>
            <div class="geometry_pointe_column2">
              <div style="display:<% if pointe.state == nil %>none<% else %>block<% end %>">
                <!-- State -->
                <legend><strong><%= l(:geometry_legend_state) %></strong></legend>
                <%= form.select(:statut, options_for_select([[l(:geometry_state_new), 1],
                                                             [l(:geometry_state_to_validated), 10],
                                                             [l(:geometry_state_resolved), 100],
                                                            ],
                                                            pointe.state)) %>
              </div>
            </div>
            <div class="geometry_pointe_column3">
              <div style="display:<% if pointe.state == nil %>none<% else %>block<% end %>">
                <!-- Comment -->
                <legend><strong><%= l(:geometry_legend_comment)%></strong></legend>
                <%= form.text_area :comment, rows: 5, cols: 50, class: 'geometry_comment', value: pointe.comment %>
              </div>
            </div>
            <div class="geometry_pointe_column4">
              <div style="display:<% if pointe.state == nil %>none<% else %>block<% end %>" class="geometry_pointe_vcenter">
                <div class="bubble" id="geometry_submit_ok_<%= pointe.id %>" style="display:none;">
                  <div class="bubble-text">
                    <p><%= l(:geometry_save_ok)%></p>
                   </div>
                </div>
                <div class="bubblenok" id="geometry_submit_failed_<%= pointe.id %>" style="display:none">
                  <div class="bubblenok-text">
                    <p><%= l(:geometry_save_failed) %></p>
                   </div>
                </div>
                <button name="commit" class="geometry_light_button" id="geometry_submit_<%= pointe.id %>" type="submit" onClick="onFormModifyPointe(event, this.form)">
                  <%= image_tag("submit.png", :plugin => "geometry_status", :title => l(:submit_statut_comment), :align => "center") %>
                </button>
              </div>
            </div>
          </fieldset>

Le code du javascript :

function onFormModifyPointe(evt, formData)
{
    // Very important!
    // If not these lines, clicking on button after moving it into a form will send the form and
    // force the page to be reloaded.
    evt = evt || window.event;

    var data = { "pointes" : {"state" : formData.pointes_state.value, "comment" : formData.pointes_comment.value } }
    var bRet = false;

    $.ajax({
        type: 'PUT',
        url: '../geometry/' + formData.pointes_id.value,
        contentType: 'application/json;charset=UTF-8',
        data: JSON.stringify(data), // access in body
        async: false, // Obligatoire pour exécuter evt.preventDefault() en cas d'échec AVANT le retour de la fonction
    }).done(function ()
    {
        document.getElementById("geometry_submit_ok_" + formData.pointes_id.value).style.display = "block";
        setTimeout(() =>
        {
          document.getElementById("geometry_submit_ok_" + formData.pointes_id.value).style.display = "none";
        }, 2000);
        bRet = true;
    }).fail(function (msg)
    {
        evt.preventDefault(); // Si échoue, ne pas faire le traitement sinon Redmine va considérer la FT comme étant sauvée même si la requête échoue (POST au lieu de PUT)
        document.getElementById("geometry_submit_failed_" + formData.pointes_id.value).style.display = "block";
        setTimeout(() =>
        {
          document.getElementById("geometry_submit_failed_" + formData.pointes_id.value).style.display = "none";
        }, 10000);
    }).always(function (msg)
    {
    });

    return bRet;
}

J'ai du d'ailleurs dû ajouter une route pour permettre au formulaire de faire un update en POST (alors qu'avant seul le PUT pour l'update était implémenté).
Du coup, sur la même fonction je peux faire un PUT ou un POST pour mettre à jour. Visiblement les formulaires ne se mettent à jour qu'en mode POST.