Boutique : la vue compacte

Nous allons modifier notre controlleur "Store" afin d'ajouter dans les vues details et compact, des tris sur plusieurs champs du modèle produit. En clair, nous pourrons trier celles-ci sur les titres, prix et date de disponibilité.

le controller

Commençons donc par le "StoreController" :

  def list
  #ordre de tri des produits
  so = sortorder
  sens = session[:sens]

  #Type d'affichage
  dm = displaymode
  case dm
    when "compact"
      @product_pages, @products = paginate :products, :per_page => 9, :order => so + " " + sens
      session[:dm] = "compact"
      render :action => "list_compact"
    when "details"
      @product_pages, @products = paginate :products, :per_page => 6, :order_by => so + " " + sens
      session[:dm] = "details"
      render :action => "list"
    else
      @product_pages, @products = paginate :products, :per_page => 9, :order => so + " " + sens
      session[:dm] = "compact"
      render :action => "list_compact"
  end
  end

Au début, vous pouvez voir 2 appels à des méthodes qui seront privée au controller: sortorder et displaytype. Ceci pour factoriser une peu notre code et le rendre plus lisible. En voici le code à glisser en fin du fichier du controller :

private 
  # permet de choisir le champ de tri pour la liste de produit.
  def sortorder
    if session[:s]==params[:s] 
      session[:sens] = (session[:sens] == "asc" ? "desc" : "asc" )
    else 
      session[:sens] = "asc"
    end
    s = params[:s] || session[:s] || "1"
    case s
      when "1"
        so = 'title'
      when "2"
        so = 'price'
      when "3"
        so = 'date_available'
    end
    session[:s] = s
    session[:prevs] = s
    return so
  end	

  # Positionne le type d'affichage souhaité pour la liste de produit.
  def displaytype
      dt = params[:dm] || session[:dm] || "compact"
  end

Nous sommes donc prêt à collecter les informations des produits, reste à les afficher...

le layout et les vues

La méthode (ou action) list est donc maintenant capable de trier et de choisir le type d'affichage. Nous devons maintenant modifier quelque-peu les vues pour le controller "Store"; en effet, une nouvelle vue fait son apparition : list_compact.rhtml. Elle est destinée à l'affichage de la vue compact des produits. Nous devrons également modifier le layout store.rhtml, pour ajouter les liens qui permettront le choix de vue et ceux des tris.

Layout gamestore/app/views/layouts/store.rhtml :

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr">
<head>
  <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
  <title><%= h "GameStore" %></title>
  <%= stylesheet_link_tag 'scaffold','store','main' %>
  <%= javascript_include_tag :defaults %>
</head>
<body>
  <div id="header">
    <h1><%= h "GameStore" %></h1>
  </div>
  <div id="main">
    <p style="color: green"><%= flash[:notice] %></p>	
    <%= render :partial => 'menu' %>
    <%= yield  %>
  </div>
  <div class="clear"></div>
  <div id="footer">
    <p><%= "Copyright &copy; 2008 - Boutique en ligne - démo - " %> <%= link_to "Contact","mailto:mcgivrer@gmail.com" %></p>
  </div>
</body>
</html>

Pour simplifier l'écriture de l'ensemble de nos liens et rendre lisible le template, nous effectuerons la mise en valeur de ceux-ci, on précise visuellement quel champ sert de critère de tri), à l'aide de helpers que nous écrirons dans le fichier gamestore/app/helpers/store_helpers.rb  :

le première servira à afficher et décorer les liens des modes d'affichage :

  def setDisplayMode(libelle,mode,title="change le mode d'affichage")
    if session[:dm]==mode
        link_to libelle,   { :action => 'list', :dm => mode}, :class=>"selected", :title=> title
    else
        link_to libelle,   { :action => 'list', :dm => mode}, :title=> title
    end
  end

le deuxième servira à laffichage des liens sur les critères de tri :

  # fixe le champ qui servira de tri
  def setSortField(libelle,value,title="Trier sur cette valeur")
    if session[:s] == value
      link_to libelle,   { :action => 'list', :s => value}, :class => "selected", :title => title
    else
      link_to libelle,   { :action => 'list', :s => value}, :title => title
    end
  end

Dans la partie #main du layout, nous ajoutons au-dessus de <%= yield %> une balise <DIV/> contenant une liste à puce qui délimitera chaque lien. La mise en forme de cette liste sera assurée par une CSS (voir gamestore/public/stylesheets/store.css.

<div id="main">
  <p style="color: green"><%= flash[:notice] %></p>	
  <div id="menu">
    <ul class="display">
      <li><%= h "Affichage" %> </li>
      <li><%= setDisplayMode "Détaillé", 'details' %></li>
      <li><%= setDisplayMode "Compact", 'compact' %></li>
      <li><%= "Trier par :" %></li>
      <li><%= setSortField "Titre", '1' %></li>
      <li><%= setSortField "Prix", '2' %></li>
      <li><%= setSortField "Disponibilité", '3' %></li>
     </ul>
     <div class="clear"></div>
   </div>
  <%= yield  %>
</div>

Utiliser le rendu partiel

Ok, nous avons une belle ligne de liens. Pour éviter de surcharger store.rhtml nous allons "partialiser" le menu en copiant le contenu délimité par la balise <div/> identifiée par "menu" dans un fichier gamestore/app/views/store/_menu.rhtml.

  <div id="menu">
    <ul class="display">
      <li><%= h "Affichage" %> </li>
      <li><%= setDisplayMode "Détaillé", 'details' %></li>
      <li><%= setDisplayMode "Compact", 'compact' %></li>
      <li><%= "Trier par :" %></li>
      <li><%= setSortField "Titre", '1' %></li>
      <li><%= setSortField "Prix", '2' %></li>
      <li><%= setSortField "Disponibilité", '3' %></li>
     </ul>
     <div class="clear"></div>
   </div>

Et en lieu et place de ces lignes, dans store.rhtml, nous allons écrire :

<%= render :partial => "menu" %>

Voila du code un peu plus professionnel. Continuons nos opérations de partialisation en pratiquant la même démarche avec les templates de list.rhtml et list_compact.rhtml: créons les fichiers _item.rhtml et _copact.rhtml qui serviront à l'affichage d'un produit dans les deux mode de liste.

List détaillée

list.rhtml

<div id="content">
  <div class="product_list">
  <% if @products %>
    <% for @product in @products %>
    <%= render :partial => 'item' %>
    <% end %>
  <% end %>
  </div>
  <div class="pager">
    <%= link_to 'Page précédente', { :page => @product_pages.current.previous }, :class =>"button prev" if @product_pages.current.previous %>
    <%= link_to 'Page suivante', { :page => @product_pages.current.next }, :class =>"button prev" if @product_pages.current.next %>
  </div>
</div>

_item.rhtml

<div class="product item">
<% if @product.image %>
  <%= image_tag url_for_file_column("product","image","mini"), :class => "product" %>
<% end %>
  <div class="infos">
    <h2><%= link_to @product.title, {:action => 'show', :id => @product.id}%></h2>
    <div class="price"><%= h fmt_currency(@product.price) %></div>
    <div class="description"><%= h @product.description%></div>
  </div>
  <div class="clear"></div>
</div>
Liste compacte :
  • list_compact.rhtml
<div id="content">
  <div class="list">
  <% for @product in @products %>
    <%= render :partial => 'compact' %>
  <% end %>
  </div>
  <div class="clear"></div>
  <div class="pager">
    <%= link_to "Page suivante".t, { :page => @product_pages.current.next }, :class =>"button prev" if @product_pages.current.next %>
    <%= link_to "Page précédente".t, { :page => @product_pages.current.previous }, :class =>"button next" if @product_pages.current.previous %>
  </div>
</div>
  • _compact.rhtml
<div class="compact">
<% if @product.image %>
  <%= image_tag url_for_file_column("product","image","mini"), :class => "cover" %>
<% end %>
  <div class="infos">
    <h2><%= link_to @product.title, {:action => 'show', :id => @product.id}, :title => (h @product.description) %></h2>
    <div class="price"><%= fmt_currency(@product.price) %></div>
    <div class="availability"><%= "Disponibilité" + "<br />" + @product.date_available.loc("%d/%m/%Y") %></div>
  </div>
  <div class="clear"></div>
</div>

Nous avons donc maintenant des affichages clair et des templates bien séparés, permettant une maintenance aisée. Pour les feuilles de styles, je vous laisse libre de parcourir les fichiers main.css, store.css, products.css.

Note :

Vous aurez sans doute remarqué que nous affichons même les simples chaînes de caractères des labels via une commande rhtml <%= "..."%>. Vous en comprendrez l'intérêt lorsque nous utiliserons le plugins de gestion du multi-linguisme Globalize.

Et donc, avec toutes ces petites modifications et les fichiers CSS ajustés vous obtiendrez cette belle page :

Liste compacte

fonction de Recherche

Un site d'achat en ligne doit permettre de trouver rapidement un produit, sans devoir parcourir toutes les page de la boutique. Une fonction de recherche s'impose donc d'elle-même. Ne réinventons pas la roue, et utilisons ce qui existe, c'est le "leit motiv" de Rails. Aussi, penchons nous sur la page "TextSearch" du wiki officiel de Rails, et utilisons la librairie proposée search.lib.

Ensuite, editer app/models/product.rb et ajouter :

__require_dependency "search"__

class Product < ActiveRecord::Base

  file_column :image,
    :magick => { :geometry=>"400x600", 
      :versions=>{ :medium => "320x200", :thumb=> "138x188", :mini=> "60x90" } }

  __searches_on :title, :description__

  validates_presence_of :title, :description

  validates_file_format_of :image, :in => ["gif", "jpg", "png"]
  validates_filesize_of :image, :in => 1.kilobytes..250.kilobytes

end

Nous avons dons précisé sur quel champs portait notre moteur de recherche. Attention, la recherche pratiqué et très simple. on recherchera la phrase passée dans ces champs.

Il nous faut donc traiter cette recherche. Commençons par ajouter un champs de recherche dans notre vue store. Comme nous connaissons les rendu partiels, profitons en et créons un _menu.rhtml dans app/layouts/store:

<% form_tag ({:controller => 'store' , :action => 'search'}, :name=>'search', :class => "search") do %>
   <input type="text" name = "searchtext" value="<%= h (params[:searchtext]||"Rechercher ...") %>" 
    cols="20"  
    onclick="javascript:this.value='';" 
    onblur="javascript:if( this.value != '' ) document.search.submit(); else this.value='<%= h (params[:searchtext]||"Rechercher ...") %>'" />
   <%= image_submit_tag "/images/icons/search.png", :value=>"" %>
<% end %>

Reprenons notre store_controller.rb et ajoutons une méthode search.

  def search
    if params[:searchtext]!=""
      @products = Product.search params[:searchtext]
    else
      flash[:notice] = "Vous devez saisir les termes de votre recherche dans la zone texte prévue à cet effet"
    end
    if !@products
      flash[:notice] = "Il n'y a pas de résultats pour la recherche '" + params[:searchtext] + "'"
    end
    render :action => "list"
  end

Ajoutons quelques test dans list.rhtml en remplaçant la div "pager" par ces lignes :

]
  <% if !params['searchtext'] %>
    <div class="pager">
      <%= link_to 'Page précédente'.t, { :page => @product_pages.current.previous }, :class =>"button prev" if @product_pages.current.previous %>
      <%= link_to 'Page suivante'.t, { :page => @product_pages.current.next }, :class =>"button prev" if @product_pages.current.next %>
    </div>
  <% else %>
  <%= link_to "Retour à la boutique".t, {:controller=>"store", :action=>"list"}, :class =>"button back" %>
  <% end %>

Et maintenant, appeler votre page http://localhost:3000/. et tester votre recherche. Vos constaterez qu'une certaine dynamique a été ajouter par le biais d'un peu de javascript sur le champs search du formulaire.

That's all !

Le Multi-linguisme

Installation du plugin Globalize

Nous aurons besoin de ce super plugin qu'est Globalize. Installons le dans notre application :

script/plugin install http://svn.globalize-rails.org/svn/globalize/branches/for-1.2

Si vous regardez dans le répertoire gamestore/vendor/plugin/ vous constaterez l'apparition d'un répertoire for-1.2. Renommez celui-ci en globalize

Configuration et utilisation de Globalize

Ensuite, lancer la commande rake globalize:setup. Celle-ci lancera la création et le remplissage de nouvelles tables dans votre base de données ( globalize_countries, globalize_languages, globalize_translations). Ces tables sont constitues le coeur du système mis en place par ce plugin.

Dans votre fichier environnement.rb, tout à la fin ajouter les lignes ci-dessous :

include Globalize
Globalize::Locale.set_base_language 'fr-FR'
Globalize::LOCALES = {'de' => 'de-DE',
           'en' => 'en-US',
           'es' => 'es-ES',
           'fr' => 'fr-FR'}.freeze

Choix automatique de la locale

Ensuite, dans ApplicationController (app/controllers/application.rb) nous allons ajouter la détection de la langue supportée par le navigateur du visiteur. Pour cela, ajoutons la méthode ci-dessous :

...
  #ne pas oublier d'automatiser l'appel
  before_filter :set_locale

  # Récupération automatique de la "locale" du navigateur.
  def set_locale
    default_locale = 'fr-FR'
    request_language = request.env['HTTP_ACCEPT_LANGUAGE']
    request_language = request_language.nil? ? nil : 
    request_language[/[^,;]+/]

    @locale = params[:locale] || session[:locale] || request_language || default_locale
    session[:locale] = @locale
    begin
      Locale.set @locale
    rescue
      Locale.set default_locale
    end
  end
...

choix manuel de la langue

Maintenant, pour rendre traductible l'ensemble de vos libellés dans vos fichier rhtml, vous devrez passer par une commande erb du type <%= "Mon libellé".t %>, ainsi, la chaine "Mon libellé" devient traductible par l'adjonction de l'appel à la méthode ".t", et une entrée dans la table globalize_translations sera créer dès le premier affichage de la page le contenant dans la langue par défaut (celle choisie dans application.rb).

Il ne nous manque plus qu'une petite interface pour choisir la langue souhaitée sur notre boutique.

Nous utiliserons une fois encore le rendu partiel _menu.rhtml depuis store.rhtml:

Ajoutez les ligne ci-dessous dans l'id "menu" :

   <ul class="translate">
    <li><%= setLinkLanguage('fr-FR',"Français".t) %></li>
    <li><%= setLinkLanguage('en-EN',"Anglais".t) %></li>
    <li><%= setLinkLanguage('de-DE',"Allemand".t) %></li>
    <li><%= setLinkLanguage('es-ES',"Espagnol".t) %></li>
   </ul>

et la méthode setLinkLanguage à placer dans application_helpers.rb :

#Défini la langue du site pour la session.
def setLinkLanguage(lang,description)
  if session[:locale]==lang
    link_to image_tag("flags/"+lang+".png"), {:controller => controller.controller_name, :action => controller.action_name, :locale => lang, :id => params[:id]}, :title=>description, :class=>"selected"
  else
    link_to image_tag("flags/"+lang+".png"), {:controller => controller.controller_name, :action => controller.action_name, :locale => lang, :id => params[:id]}, :title=>description
  end
end

Constatez l'apparition des .t dans tous les libellés dans les codes sources livrés en exemple.

Une interface de gestion des traductions

L'article situé à cet endroit est un excellent tutorial pour la mise en place ultra rapide d'une interface de traduction. je vous laisse seul juge.

  • Controller app/controllers/admin/translate_controller.rb
class Admin::TranslateController < ApplicationController

  layout "admin"

  def index
    @view_translations = ViewTranslation.find(
      :all, 
      :conditions => [ 'text IS NULL AND language_id = ?', Locale.language.id ], 
      :order => 'tr_key')
  end

  def translation_text
    @translation = ViewTranslation.find(params[:id])
    render :text => @translation.text || ""  
  end

  def set_translation_text
    @translation = ViewTranslation.find(params[:id])
    previous = @translation.text
    @translation.text = params[:value]
    @translation.text = previous unless @translation.save
    render :partial => "translation_text", :object => @translation.text  
  end
end

Ensuite, passons aux templates de rendu :

  • app/views/admin/translate
<div class="translations">
<% base_language_only do -%>
  <div id="language"><h1>Please choose language for translation</h1></div>
<% end -%>

<% not_base_language do -%>
  <div id="language"><h1><%= "Language: " + Locale.language.native_name %></h1></div>
  <div>
    <% @view_translations.each do |tr| -%>
      <%= render :partial => 'translation_form', :locals => {:tr => tr}%>
    <% end -%>
  </div>
<% end  -%>
</div>
  • app/views/admin/translate/_translation_text.rhtml : L'affichage de la traduction d'un libellé dans la langue sélectionnée:
<%= translation_text || '[no translation]' %>
  • app/views/admin/translate/_translation_form.rhtml : Le formulaire de saisie via un "edit in place" de la traduction d'un libellé:
<!--[form:translate]-->
<p class="item">
<label for="tr_<%= tr.id %>"><%=tr.tr_key%></label><br />
<span id="tr_<%= tr.id %>" class="translation">
<%= render :partial => 'translation_text', :object => tr.text %>
</span>
<%= in_place_editor "tr_#{tr.id}", 
    :url => { :action => :set_translation_text, :id => tr.id },
    :load_text_url => url_for({ :action => :translation_text, :id => tr.id })%>
</p>
<!--[eoform:translate]-->
  • app/layouts/admin.rhtml enfin, le layout d'affichage des outils d'administration (/admin) :
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
    <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
    <title><%= "Administration du site".t %></title>
    <%= stylesheet_link_tag 'scaffold','main','admin','product' %>
    <%= javascript_include_tag :defaults %>
  </head>
  <body>
    <div id="header">
      <h1>GameStore</h1>
      <h2><%= "Administration".t %></h2>
    </div>
    <div id="main">
      <div id="sidebar">
        <ul>
          <li><%= link_to "Boutique".t, { :controller => '/store', :action=>'index' } %></li>
          <li><%= link_to "Produit".t, { :controller => 'admin/product', :action=>'index' } %></li>
          <li><%= link_to "Traduction".t, { :controller => 'admin/translate', :action=>'index' } %></li>
        </	ul>
      </div>
      <div id="content">
        <p class="notice"><%= flash[:notice] %></p>
        <%= yield  %>
      </div>
      <div class="clear"></div>
    </div>
    <div id="footer">
      <p><%= "Copyright &copy; 2008 - Boutique en ligne - démo - ".t %>
<%= link_to "Contact".t,"mailto:mcgivrer@gmail.com" %></p>
    </div>
  </body>
</html>

Enfin, on ajoute quelques définitions dans notre feuille de styles public/stylesheets/admin.css:

/*---- traduction du site ----*/
.translations *{
	padding:2px:
	margin:2px;
}
.translations .item {
	border-bottom:1px dotted #ddd;
	margin:4px;
	padding:2px;
}
.translations .item label{
	font-weight: bold;
}
.translations .item label .number{
	font-size: 7pt;
}
.translations .item .translation {
	color:navy;
	padding:2px;
	margin:4px;
	border:1px solid #ddd;
}

Et voilà ! appeler l'url http://localhost:3000/admin/translate/?locale=es-ES et vous serez en position de saisir la traduction en espagnol (es-ES) de tous les libellés non-encore traduits.

Liste des libellé à traduire

pour editer un des libellés, passer votre souris sur celui-ci, il apparait alors en surligné jaune:

Edition d'un libellé

Un clic et vous voilà en édition sur celui.

Modification d'une valeur