Créer une boutique en ligne - Partie 4 : Vue compact, recherche et multi-linguisme.
Par McGivrer,
samedi 5 janvier 2008 à 13:30
:: Ruby on Rails
:: #38
:: rss
Bon, nous avons déjà un bon début, la gestion des produits, la vue "boutique" détaillée. Nous allons étailler notre site avec une vue , plus compacte, et même ajouter une fonction "recherche". Enfin, nous passerons donc aux choses sérieures avec la mise en place du plugin Globalize pour ajouter le multi-linguisme à notre site. On en profitera pour passer un coup de polish sur l'ensemble des feuilles de styles, des templates rhtml et du code. Aller, HOP au boulot !
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 © 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 :
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.rhtmlenfin, 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 © 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.
pour editer un des libellés, passer votre souris sur celui-ci, il apparait alors en surligné jaune:

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



Commentaires
Aucun commentaire pour le moment.
Ajouter un commentaire