jeudi 25 juin 2009

Gestion de groupes d'utilisateurs


Billet centré cette fois la gestion des groupes d’utilisateurs (en prévision de la gestion des rôles).
Plusieurs techniques sont abordées ici :
- nested routes, resource & form
- multiple will_paginate sur une page
- ajax / rjs et effets scriptaculous
- in_place edit
- autocomplete avec renvoi d’id au lieu d’un string

NB : j’ai ouvert un dépôt Github (lien à droite) pour tracer le code et vous permettre de tester directement (cliquez sur ‘download’, décompressez et lancez directement à la racine mongrel_rails start –p 80. La config admin est dans la création de la table users (en attendant Rails v 3.0, ou il y aura un fichier assets.rb spécialement pour les configurations initiales).

La prochaine fois gestion des droits utilisateurs avec declarative_authorization.





1.Objectifs de l’article
Nous avons pour le moment 2 classes principales : User et Wiki. Pour préparer la gestion des droits d’accès plus finement, nous avons besoin de créer des groupes d’utilisateurs (pouvant contenir un ou plusieurs sous-groupes).

2.Préparation
Trois plugin seront utilisés, principalement pour faciliter une saisie plus naturelle coté utilisateur : will_paginate (pour la pagination), auto_complete (permet de suggerer une liste correspontante au texte frappé) et in_place_editing (pour editer sans passer par la page update).
Tous les trois sont relativement anciens (ils étaient directement intégrés à Rails 1.x) mais font ce qu’on leur demande.
Will_paginate s’intalle en gem (donc n’oubliez pas de mettre à jour le fichier environment.rb), les deux autres en plugins uniquement.

Une fois installer, on prépare les tables elles-même : Group, Group_member et Group_subgroup :
>ruby script/generate scaffold groups name:string user:references
>ruby script/generate model group_member group:references user:references
>ruby script/generate model group_subgroup group:references subgroup:references


3. Modèles

Voici les 3 modèles :
app/models/group.rb :
class Group < ActiveRecord::Base
belongs_to :user
has_many :group_members, :dependent => :destroy
has_many :group_subgroups, :dependent => :destroy
has_many :subgroup, :class_name => 'GroupSubgroup', :foreign_key => 'subgroup_id'
validates_uniqueness_of :name

def auto
end
end

app/models/group_member.rb :
class GroupMember < ActiveRecord::Base
belongs_to :group
belongs_to :user

validates_uniqueness_of :user_id, :scope => :group_id
end

app/models/group_subgroup.rb
class GroupSubgroup < ActiveRecord::Base
belongs_to :group
belongs_to :subgroup, :class_name => 'Group', :foreign_key => 'subgroup_id'

validates_uniqueness_of :group_id, :scope => :subgroup_id
end


Et le modèle user qui est modifié lui aussi por prendre en compte la dépendance :
  has_many :group_members
has_many :groups # group creator


En dehors des relations has_many/belongs_to classiques entre les 3 tables, il y a un premier point intéressant :
 validates_uniqueness_of :user_id, :scope => :group_id 

Cette déclaration permet de vérifier qu’il n’y a qu’un seul même utilisateur (user_id) par groupe (group_id).
Ensuite :
 belongs_to :subgroup, :class_name => 'Group', :foreign_key => 'subgroup_id'

Et sa correspondance dans la table group permet de définir une clef étrangère, puisque dans la table goup_subgroup, group_id et subgroup_id pointent tout deux vers la table group.

Dernier point : la définition de la méthode ‘auto’ dans la table group est la seule réponse rapide rapide que j’ai trouvé à un bug sur le auto_complete...

4. Routes
Pour mettre en places les routes :
  map.resources :users, :collection => { :list => :get } 
map.resources :groups, :has_many => [:group_members, :group_subgroups], :collection => { :list => :get }

L’ajout des collection servira pour les auto_complete ci-dessous ; la déclaration has_many sert à indiquer des resources sous-jascente.
Pour voir toutes les routes ainsi crées, faite rake routes.

5. In_place_editing
Utilisation très aisé du plugin (pas trop REST mais bon) : déclaration dans le controleur Group :
class GroupsController < ApplicationController
before_filter :require_user
in_place_edit_for :group, :name

Et utilisation dans la vue :
%h1 Groups
%h3 Show Group
%p
%b Name :
= in_place_editor_field :group, 'name'

Et c’est tout !

5. Auto_complete, Nested Form
Le plugin auto_complete fait un appel javascript pour trouver la liste de suggestion qu’il doit afficher. Dans le champs suivant nous voulons ajouter un utilisateur à un group (donc insérer un enregistrement dans group_member ensuite) :
    Add user :
= text_field_with_auto_complete :user, :auto, {:size => 25, :autocomplete => 'off'}, |
{:skip_style => true, :frequency => 0.25, |
:url => list_users_path, :method => :get, :with => "'user_query=' + element.value"} |

(les barres de fin sont une astuce concernant les lignes longues sous Haml).
text_field_auto_complete prend 4 paramètre : modèle visé, champ du modèle, options html, options auto_complete.
Il est possible de l’utiliser en mode ‘par défaut’, et dans ce cas là vous devez créer une fonction spécifique dans le controleur group renvoyant une liste d’utilisateur.
L’option html :autocomplete => 'off' permet de dire au browser de ne pas autocompléter lui-même le champ avec les valeurs qu’il garde en mémoire.
Comme c’est mieux de laisser le controlleur User faire le boulot pour lequel il est fait, j’ai spécifié l’url où il faut aller chercher les données : ici :url => list_users_path . Ls 2ème paramètres est donc complètement inutile, mais il faut les spécifier tout de même.

Construction de la méthode dans le controleur User (qu’il serait possible de fusionner avec la méthode index en faisant un test sur les paramètres d’entrées) :
  def list
@query = params[:user_query]
@users = User.find :all, :conditions => ['username LIKE ? OR lastname LIKE ?', "%#{@query}%", "%#{@query}%"]
respond_to do |format|
format.html
format.js { render :partial => 'list', :layout => false }
end
end

params[:user_query] est le paramètre passé par :with => "'user_query=' + element.value" Et la méthode renvoi en retour un partial. A noter que le contenu du partial doit impérativement être de la forme d'une liste ul / li.

app/views/users/list.js.haml
%ul
- @users.each do |u|
- text = u.entire_name
%li<
= link_to_function highlight(text,@query), "$('group_member_user_id').value = '#{u.id}';", :style =>"display:block;"


Deux astuces ici : d’abord %li< le signe inférieur indique à Haml de ‘coller’ les éléments li ... sans quoi vous aurez des espaces blancs avant et après le nom (il doit manquer un strip dans le plugin quelque part). Faites l’essai avec et sans vous verrez.

Ensuite on ne retourne pas directement le nom d’un User, mais une fonction javascript qui va modifier la valeur d’un champ 'group_member_user_id en y plaçant l’id de l’utilisateur. :
= f.hidden_field  :user_id

Ce champs est caché, mais permet de ne rien modifier à la méthode update standard lors du submit, sans avoir à rechercher l’utilisateur correspondant au nom renvoyé normalement.

D’où le formulaire final :
  %h3 Users member list
- remote_form_for [@group, GroupMember.new] do |f|
Add user :
= text_field_with_auto_complete :user, :auto, {:size => 25, :autocomplete => 'off'}, |
{:skip_style => true, :frequency => 0.25, |
:url => list_users_path, :method => :get, :with => "'user_query=' + element.value"} |
= f.hidden_field :user_id
= f.hidden_field :group_id, :value => @group.id
= f.submit

Notez l’appel particulier remote_form_for (et inspecter le nom des élément avec Firebug sous Firefox).

Une petite mise en forme ajoutée dans public/stylesheets/red.sass : (puisque qu’on utilise pas le css fournit avec auto_complete) :
// ------------------ autocomplete
div.auto_complete
:width 350px
:background #fff
ul
:border 1px solid #888
:margin 0
:padding 0
:width 100%
:list-style-type none
li
:margin 0
:padding 3px
&.selected
:background-color #ffb
a
:text-decoration none
&:hover
:background-color #ffb
:color black
strong.highlight
:color #800
:margin 0
:padding 0


Maintenant on veut afficher la liste des membres du groupes sous le formulaire :
= render @list_group_members

@list_group_member est défini dans le controleur gérant la vue :
@list_group_members = @group.group_members

render @list_group_members équivaut à render :partial => 'group_members/group_member', :collection => @list_group_members, ou encore @list_group_members.each{|g| render :partial => 'group_members/group_member', :object => g}
Cette nouvelle notation plus simple des render à été introduite avec Rails 2.3.2
Le partial est tout simple :
 %li#gp_user_id{:id => "#{group_member.id}" }
= link_to group_member.user.full_name, user_path(group_member.user)
= link_to_remote "[-]", :url => group_group_member_path(@group, group_member) , :method => :delete, :confirm => 'Are you sure ?'

Il met en place un lien pour supprimer directement un membre d’un groupe. On met un id sur l’élément li pour permettre d’y faire référence dans la suppression du membre ; à noter le path qui est de cette forme [modele parent]_[modele enfant]_path du fait de la délaration des routes.
Voici la fonction destroy appellée du controlleur app/controllers/group_members_controller.rb :
  def destroy
@group_member = GroupMember.find(params[:id])
@group_member.destroy

respond_to do |format|
format.html { redirect_to(group_members_url) }
format.xml { head :ok }
format.js { render :update do |page|
page.visual_effect :drop_out, "gp_user_id_#{params[:id]}"
end
}
end
end

Et c’est là que l’id sur l’élément li est utilisé, puisque qu’il est ‘effacé’ de l’écran par l’appel à page.visual_effect.

Revenons sur la création d’un membre, où là aussi on ajoute des effets ajax :
app/controllers/ group_members_controller.rb :
  def create
@group_member = GroupMember.new(params[:group_member])

respond_to do |format|
if @group_member.save
format.js {render :update do |page|
page.insert_html :bottom, 'wp_user_list', :partial => 'group_member', :object => @group_member
page.visual_effect :highlight, "gp_user_id_#{@group_member.id}", :duration => 5
page[:user_auto].value = ''
end }
else
format.js { render :nothing => true }
end
end
end

Cette fois on ajoute le nouveau membre à la fin de l’élément div spécifié (wp_user_list), on le met en valeur et on finit en effacant le contenu d la boite de saisie.

6. Will_paginate
On repete les mêmes opérations pour les sous-groupes d’un groupe (un groupe administrateur peut ainsi contennir le groupe wiki_managers par exemple) et on fait tenir le tout sur un même formulaire :

%h1 Groups
%h3 Show Group
%p
%b Name :
= in_place_editor_field :group, 'name'
%p
%b Created by
= @group.user.full_name
at
= @group.created_at.to_s :short
%br{:style => 'clear:both;'}
#group_user_member{:style => 'float: left;width:450px;'}
%h3 Users member list
- remote_form_for [@group, GroupMember.new] do |f|
Add user :
= text_field_with_auto_complete :user, :auto, {:size => 25, :autocomplete => 'off'}, |
{:skip_style => true, :frequency => 0.25, |
:url => list_users_path, :method => :get, :with => "'user_query=' + element.value"} |
= f.hidden_field :user_id
= f.hidden_field :group_id, :value => @group.id
= f.submit
%ol#wp_user_list
= render @list_group_members
= will_paginate @list_group_members, :param_name => :members_page

#group_group_member{:style => 'margin-left:500px;width: 450px;'}
%h3 Groups member list
- remote_form_for [@group, GroupSubgroup.new] do |f|
Add subgroup :
= text_field_with_auto_complete :group, :auto, {:size => 25, :autocomplete => 'off'}, |
{:skip_style => true, :frequency => 0.25, |
:url => list_groups_path, :method => :get, :with => "'group_query=' + element.value"} |
= f.hidden_field :subgroup_id
= f.hidden_field :group_id, :value => @group.id
= f.submit
%ol#wp_sub_list
= render @list_group_subgroups
= will_paginate @list_group_subgroups, :param_name => :subgroups_page
%br{:style => 'clear:both;'}

= link_to 'Back', groups_path


Deux boites donc : une avec les membres à gauche et une autre à droite avec les sous-groupes. C’est là qu’apparait le bug qui nécesite la définition de la méthode ‘auto’ pour le modèle Group, car le second auto_complete la réclame absolument (alors que celui sur les utilisateurs non ... ??).

On a donc 2 listes (users et subgroups) qui doivent être paginées si elles sont trop longues. D’où le
= will_paginate @list_group_subgroups, :param_name => :subgroups_page 

Avec deux nom de param_name différents pour permettre justement de les différencier :)

Il faut par contre mettre à jour dans le controlleur la définition de la liste :
app/controllers/groups_controller.rb
@list_group_subgroups = @group.group_subgroups.paginate :page => params[:subgroups_page], :per_page => 10 


L’appel complet de la méthode devient donc :
  def show
@group = Group.find(params[:id])

@list_group_subgroups = @group.group_subgroups.paginate :page => params[:subgroups_page], :per_page => 10
@list_group_members = @group.group_members.paginate :page => params[:members_page], :per_page => 10
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => @group }
end
end



7. Conclusion
Voilà, tout est prêt maintenant pour gérer le droits d’accès dans la prochaine étape. Le code est téléchargeable (tag version 0.1) sur Github avec les plugins installés pour vous permettre de tester tout ça rapidement. (A voir si il ne faut pas faire un rake db:drop / rake db:create / rake db:migrate et rake gems:install d’abord ... je vérifierai pour les prochaines releases).



Read More......

dimanche 21 juin 2009

Base d'un wiki avec éditeur wysiwyg, gestionnaire de version et pagination


Troisième partie avec l’intégration rapide de la pagination (will_paginate), la mise en place d’un simili wiki (les wikiwords ne sont pas encore mis en place) avec un éditeur WYSIWYG : tinyMCE, complété par un gestionnaire de version et une solution de remplacement : Textile.

Prochaine partie : mise en place des commentaires avec acts_as_comment.




1. Préparation
Téléchargez le plugin de Kete, décompressez le dans le répertoire vendor/plugin/tiny_mce et à la racine du projet :
>rake tiny_mce :install

Tous les fichiers de la version actuelle (3.2.4.1) seront mis en place.

Pour Will_paginate, installez la gem :
>gem install mislav-will_paginate

Et déclarez-là dans le fichier environment.rb
Rails::Initializer.run do |config|
config.gem "authlogic"
config.gem "justinfrench-formtastic", :lib =>'formtastic', :source =>'http://gems.github.com'
config.gem 'mislav-will_paginate',  :lib => 'will_paginate', :source => 'http://gems.github.com'

Pour act_as_versioned, l’installation de la gem ne fonctionne pas ( ??), donc on télécharge le plugin et on l’installe directement sous vendor/plugin.
Enfin, même procédé pour acts_as_textiled : sous vendor/plugin.

2. Wiki
Un simple modèle suffira pour l’instant, avec une différenciation créateur/éditeur de la page :
>ruby script/generate scaffold wiki title :string page:text owner_id:integer writer_id:integer

Modification du modèle wiki :
class Wiki < ActiveRecord::Base
belongs_to :owner, :class_name => "User", :foreign_key => "owner_id"
belongs_to :writer, :class_name => "User", :foreign_key => "writer_id"  
end

Et du modèle User (j’en profite pour créer un champ virtual pour facilité la lecture des noms/prénoms) :
class User < ActiveRecord::Base

acts_as_authentic do |c|
c.login_field = :ident
end

has_many :wiki_owner, :class_name => "Wiki", :foreign_key => "owner_id"
has_many :wiki_writer, :class_name => "Wiki", :foreign_key => "writer_id"

def full_name
n = self.lastname.nil? ? "" : self.lastname.split('-').map{|x| x.first.capitalize}.join('.')
n + ". " + self.username.capitalize
end

end

Le controller du wiki ne bouge pas trop, sauf les mise à jours des champs owner/writer sur la creation et l’update (à noter la restriction before_filter et la préparation des wikiword) :
class WikisController < ApplicationController
CamelCase = Regexp.new( '\b((?:[A-Z]\w+){2,})' )
before_filter :require_user

# GET /wikis
def index
@wikis = Wiki.all
end

# GET /wikis/1
def show
@wiki = Wiki.find(params[:id])
if params[:version_id]
@wiki.revert_to params[:version_id]
end
end

# GET /wikis/new
def new
@wiki = Wiki.new
end

# GET /wikis/1/edit
def edit
@wiki = Wiki.find(params[:id])
end

# POST /wikis
def create
@wiki = Wiki.new(params[:wiki])
@wiki.owner = current_user
@wiki.writer = current_user
if @wiki.save
flash[:notice] = 'Wiki was successfully created.'
redirect_to @wiki
else
render :action => "new"
end
end

# PUT /wikis/1
def update
@wiki = Wiki.find(params[:id])
@wiki.writer = current_user
if @wiki.update_attributes(params[:wiki])
flash[:notice] = 'Wiki was successfully updated.'
redirect_to @wiki
else
render :action => "edit" 
end
end

# DELETE /wikis/1
def destroy
@wiki = Wiki.find(params[:id])
@wiki.destroy
redirect_to wikis_url
end
end

Le modèle est fonctionnel et vous pouvez le tester sur le site ou dans la console. Avant de modifier les vue, intégrons la partie versionning du wiki.

3. Versionning
Le versionning est très simple à mettre en place :
>ruby script/generate migration WikiVersions

Puis modification du fichier de migration généré :
class CreateWikiVersions < ActiveRecord::Migration
def self.up
Wiki.create_versioned_table
end

def self.down
Wiki.drop_versioned_table
end
end
N’oubliez pas le rake db:migrate. Intégration dans le modèle Wiki app/models/wiki.rb:
class Wiki < ActiveRecord::Base
belongs_to :owner, :class_name => "User", :foreign_key => "owner_id"
belongs_to :writer, :class_name => "User", :foreign_key => "writer_id"

acts_as_versioned :if_changed => [:title, :page]  
def writers
self.versions.map(&:writer_id).uniq
end
end
La fonction writers servira à afficher la liste des rédacteurs. Ajout d’une function permettant de changer de version dans app/controllers/wikis_controller.rb qu’on utilisera dans les vues :
def revert_to_version
@wiki = Wiki.find(params[:id])
@wiki.revert_to!(params[:version_id])
redirect_to @wiki
end
4. Les vues Copier coller des vues, adoptant la même structure que les vues User : app/views/wikis/index.html.haml :
%h1 Wikis
%h3 Listing Wikis
%table.table_index
%thead
%tr
%th Title
%th Owner
%th Writer
%th Version
%th 
%tbody   
- @wikis.each do |w|
%tr{ :class => cycle('even', 'odd')}
%td= w.title
%td= w.owner.full_name
%td= w.writer.full_name
%td= w.version
%td
= link_to 'Show', w
= link_to 'Edit', edit_wiki_path(w)
= link_to 'Destroy', w, :confirm => 'Are you sure?', :method => :delete
%br
= link_to 'New wiki', new_wiki_path
app/views/wikis/new.html.haml :
%h1 Wiki
%h3 New Wiki
#bloc_form
= render @wiki
%br
= link_to 'Back', wikis_path 
app/views/wikis/edit.html.haml :
%h1 Wiki
%h3 Edit Wiki
#bloc_form
= render @wiki
%br
= link_to 'Back', wikis_path
app/views/wikis/_wiki.html.haml :
- semantic_form_for @wiki do |f| 
= f.error_messages
- f.inputs do
= f.input :title
= f.input :page
- f.buttons do
= f.commit_button
app/views/wikis/show.html.haml :
%h1 Wiki
%h3= @wiki.title
#wiki_page
=@wiki.page
#wiki_foot 
Owner
= link_to @wiki.owner.full_name, user_path(@wiki.owner)
Writer
- User.find(@wiki.writers).each do |w|
= link_to w.full_name, user_path(w)
%br
Versions
- for v in @wiki.versions.reverse
= "["+ v.version.to_s + ":"
= link_to 'show', wiki_path(@wiki, :version_id => v.version)
= link_to 'revert', :action => 'revert_to_version', :version_id => v.version, :id => @wiki
= "]"
%br
= link_to 'Edit', edit_wiki_path(@wiki)
|
= link_to 'Back', wikis_path 
Et un petite modif pour le css du pied de page wiki (à rajouter à la fin de public/stylesheets/sass/red.sass ) :
// ------------------ wiki
#wiki_foot 
:background-color #FEC
:border 2px solid #DBCCB6
:margin-top 120px
:padding 12px
Voilà le gestionnaire de version est maintenant fonctionel. Reste à intégrer acts_as_textiled, tinyMCE, et will_paginate. 5. Acts_as_textiled Textile est un format d’édition rapide (voir Redcloth). Le plugin acts_as_textiled permet de l’utiliser rapidement sur toutes les vues (dans les champs string et textarea) sans les modifier. Il suffit de modifier le modèle comme suit : app/models/wiki.rb
class Wiki < ActiveRecord::Base
belongs_to :owner, :class_name => "User", :foreign_key => "owner_id"
belongs_to :writer, :class_name => "User", :foreign_key => "writer_id"  

acts_as_versioned :if_changed => [:title, :page]  
acts_as_textiled :page

def writers
self.versions.map(&:writer_id).uniq
end
end
Acts_as_textiled peut ainsi être rapidement implentés dans tous vos futures modèles. 6. Will_paginate Pour la pagination, rien de plus simple là aussi après avoir installé le plugin ou la gem. Un exemple complet se trouve sur RailsCast épisode 51. Pour les mettre sur la vue users, on modifie d’abord le controller app/controllers/users_controler.rb, la méthode index :
def index
@users = User.paginate :page => (params[:page]||1), :order => 'username ASC', :per_page => 10
respond_to do |format|
format.html # index.html.erb
format.xml  { render :xml => @users }
end
end
et on rajoute en fin de fichier de la vue app/views/users/index.html.haml :
%br
= will_paginate @users
= link_to 'New user', new_user_path 
Même chose pour le wiki :
#dans le controller wikis_controller.rb, methode index
def index
@wikis = Wiki.all
end
# devient
def index
@wikis = Wiki.paginate :page => (params[:page]||1), :order => 'title ASC', :per_page => 10
end
# et on rajoute dans le fichier app/views/wikis/index.html.haml la ligne :
= will_paginate @wikis
Reste une petite modification rapide du css (toujours public/stylesheets/sass/red.sass) :
// ------------------ pagination  
.pagination
:padding-top 20px
//:text-align center
a
:padding 2px
:border 2px solid #DBCCB6
:font-weight normal
:background-color #FEC
:color #000
:text-decoration none
&:hover, &:active
:color #FFF
:background-color #A00
span
&.current, &.disabled
:padding 2px
:border 1px solid #000
:background-color #FFF
:color #000
7. TinyMCE Le plugin correctement installé (voir partie 1 au début), il reste à l’intégrer : Modifiez le layout principal pour prendre en compte les fichiers de TinyMCE (dans app/views/layout/application.html.haml) en rajoutant la ligne dans l’en-tête = include_tiny_mce_if_needed :
!!!
%html
%head
%title Red
= stylesheet_link_tag 'scaffold'
= stylesheet_link_tag 'formtastic'
= stylesheet_link_tag 'red'
= stylesheet_link_tag 'login' if not current_user
= javascript_include_tag :defaults
= include_tiny_mce_if_needed
%body
= render :partial => 'shared/head' if current_user
%p.flash_notice= flash[:notice]
#content= yield
Puis dans le controlleur qui l’utilise (app/controllers/wikis_controller.rb) rajoutez uses_tiny_mce :
class WikisController < ApplicationController
CamelCase = Regexp.new( '\b((?:[A-Z]\w+){2,})' )
before_filter :require_user
uses_tiny_mce

# GET /wikis
def index
...
Et enfin de modifier la vue en changeant juste la classe du textarea :
- semantic_form_for @wiki do |f| 
= f.error_messages
- f.inputs do
= f.input :title
= f.input :page, :input_html => { :class => 'mceEditor', :style => 'width:800px' }
- f.buttons do
= f.commit_button
Et ça fonctionne :) vous pouvez utiliser aussi du code textile à l’intérieur, il sera correctement mis en valeur. Pour mettre quelques options standard (voir la doc sur le site de TinyMCE), on peut faire par exemple (dans le controller du wiki) :
uses_tiny_mce :only => [:new, :create, :edit, :update], :options => {
:theme => 'advanced',
:theme_advanced_resizing => true,
:theme_advanced_resize_horizontal => false,
:plugins => %w{table fullscreen contextmenu},
:theme_advanced_toolbar_align => 'left',
:theme_advanced_toolbar_location => 'top',
:theme_advanced_buttons1 => 'undo,redo,cut,copy,paste,pastetext,|,bold,italic,strikethrough,blockquote,charmap,bullist,numlist,removeformat,|,link,unlink,image,|,cleanup,code',
:theme_advanced_buttons2 => 'formatselect,fontselect,fontsizeselect,|,justifyleft,justifycenter,justifyright,indent,outdent,|,forecolor,backcolor,|,table,fullscreen',
:theme_advanced_buttons3 => ''
}
TODO - speelchecking pour TinyMCE, - ajax sur les liens de paginations will_paginate, - et wiki complet (wikiword, pages spéciales, etc ...)
Read More......

vendredi 19 juin 2009

Haml, Formtastic et Authlogic


Deuxième partie couvrant l'installation de deux gems usuelles (haml et formtastic) et la mise en place du traitement des autorisations d'accès au site (avec la librairie authlogic).

Prochaine partie : will_paginate, tinyMCE, acts_as_versionned pour un début de wiki home made.




1. Préparation
Deux librairies Haml et Formtastic sont installées au départ car elles sont utilisées en parmanence dans la suite du projet.

Haml permet de simplifier et clarifier les vues html, il s'installe simplement par :
>gem install haml
>haml --rails path/to/rails/app

Formtastic est là aussi pour simplifier la génération des formulaires de saisie (la seconde commande génére 2 fichiers css pour la mise en forme des formulaires) :
>gem install justinfrench-formtastic
>ruby script/generate formtastic_stylesheets

Deux choix pour l'authentification : restfull_authenticate et authlogic. J'ai choisi le second car il me semblait plus simple et plus évolutif. NB : ne pas confondre authentification (identifier un utilisateur) et authorisation (identifier les droits d'un utilisateur).
Donc :
>gem install authlogic

Puis modification du fichier de configuration pour inclure les gems (fichier /config/environment.rb) :
Rails::Initializer.run do |config|
config.gem "authlogic"
config.gem "justinfrench-formtastic", :lib =>'formtastic', :source =>'http://gems.github.com'
config.active_record.timestamped_migrations = false

La dernière ligne permet de raccourcir le nom des fichiers de migrations (comme avec Rails 1.x).


2. Création du model User
Une façon simple de commencer avec authlogic est de partir de son exemple de base mais pour comprendre un peu plus le fonctionnement j'ai refait le boulot (DRY oui, mais une fois qu'on a compris :) ... ).

Création d'un model utilisateur avec les champs nécessaires à authlogic : comme on se servira des vues de bases au démarrage, on lance un scaffold avec les champs principaux :
>ruby script/generate scaffolf user username:string
lastname:string login:string password:string email:string

et modifier ensuite le fichier de migration généré (dans /db/migrate) comme suite :

class CreateUsers < ActiveRecord::Migration
def self.up
create_table :users do |t|
t.string :username, :email, :ident, :null => false
t.string :lastname, :last_login_ip, :current_login_ip
t.string :crypted_password, :password_salt, :persistence_token
t.datetime :last_request_at, :last_login_at, :current_login_at
t.integer :login_count, :failed_login_count
t.string :perishable_token, :default => "", :null => false
t.boolean :remember_me
t.timestamps
end

add_index :users, :ident
add_index :users, :persistence_token
add_index :users, :last_request_at
add_index :users, :email

u=User.new
u.password='admin'
u.password_confirmation = 'admin'
u.password_salt
u.id = 1
u.username = 'Admin'
u.ident = 'red'
u.email = "admin@red.net"
u.save
end

def self.down
drop_table :users
end

end

Deux points sur ce fichier : j'ai ajouté un premier utilisateur pour éviter d'avoir à le faire après chaque migration et pour pouvoir tester tout de suite avec un login correct (à ne pas faire en production). Attention, la migration ne pourra se faire qu'une fois complétée le modèle user. J'ai rajouté un champ "ident" qui servira pour la connexion. Les autres champs sont remplis automatiquement par authlogic. Les autres champs sont automatiquement interprétés et utilisés par le plugin (email, password, etc : voir les MagicColumns dans la doc.).

Définition du model app/models/user.rb pour prendre en compte authlogic (le champs utilisé pour le login est spécifié ici, voir la doc pour les options supplémentaires) :
class User < ActiveRecord::Base
acts_as_authentic do |c|
c.login_field = :ident
end
end

Et petite modification du controller app/controller/users_controller.rb afin de restreindre l’accès aux utilisateurs connectés :
class UsersController < ApplicationController
before_filter :require_user
...

Les vues standards (index, edit, new, show : CRUD) ont été crées aussi : dans /app/views/users avec une structure de base fonctionnelle.
La route à été ajoutée au fichier config/routes.rb
map.ressources users


3. Création de la session

Les sessions permettent à un utilisateur de naviguer sur le site en étant authentifié. On peut les stocker en tant que fichiers ou bien dans la base de données. Ayant choisit la 2ème possibilité il faut donc modifier la configuration config/environment.rb et rajouter une ligne :
Rails::Initializer.run do |config|
...
config.action_controller.session_store = :active_record_store

Il faut ensuite définir le le modèle, le controleur et la migration:
>ruby script/generate session user_session
>ruby script/generate controller user_sessions
>ruby script/generate session_migration


Le modèle a besoin d’être créé simplement : app/views/model/user_session.rb
class UserSession < Authlogic::Session::Base
End

Il faut implémenter une méthode pour se connecter et pour se déconnecter (new et destroy) dans user ( attention au pluriel de UserSessions ! regardez dans la doc les méthodes pluralize et singularize).
Implémentation des fonctions login et logout dans user_sessions_controller.rb :
class UserSessionsController < ApplicationController
before_filter :require_no_user, :only => [:new, :create]
before_filter :require_user, :only => :destroy
def new
@user_session = UserSession.new
end

def create
@user_session = UserSession.new(params[:user_session])
if @user_session.save
flash[:notice] = "Login successful!"
redirect_to root_url
else
render :action => :new
end
end

def destroy
current_user_session.destroy
flash[:notice] = "Logout successful!"
redirect_to root_url
end

end

Les before_filter sont explicites et restreignent donc l’accès (comme pour le controller User).
A noter aussi l'appel d'une fonction encore non définie :current_user_session qui fait partie d'un ensemble de quelques fonctions très utiles pour la persistence d’une session. Elles sont définies directement dans application_controler.rb qui devient :
# Filters added to this controller apply to all controllers in the application.
# Likewise, all the methods added will be available for all controllers.

class ApplicationController < ActionController::Base
helper :all # include all helpers, all the time
protect_from_forgery # See ActionController::RequestForgeryProtection for details

# Scrub sensitive parameters from your log
helper_method :current_user_session, :current_user
filter_parameter_logging :password, :password_confirmation

private
def current_user_session
return @current_user_session if defined?(@current_user_session)
@current_user_session = UserSession.find
end

def current_user
# return @current_user if defined?(@current_user)  equivalent au ||= en-dessous
@current_user ||= current_user_session && current_user_session.record
end

def require_user
unless current_user
store_location
flash[:notice] = "You must be logged in to access this page"
redirect_to new_user_session_url
return false
end
end

def require_no_user
if current_user
store_location
flash[:notice] = "You must be logged out to access this page"
redirect_to login_url
return false
end
end

def store_location
session[:return_to] = request.request_uri
end

def redirect_back_or_default(default)
redirect_to(session[:return_to] || default)
session[:return_to] = nil
end
end
(copier/coller du tutorial authlogic).
La méthode current_user est appelée très souvent par la suite : pour retrouver un utilisateur déjà connecté la fonction principale est UserSession.find.

Pour le fichier routes.rb on aura finalement :
#   map.login  'login',   :controller => 'user_sessions', :action => 'new'
map.logout 'logout', :controller => 'user_sessions', :action => 'destroy'
map.signup 'signup', :controller => 'users', :action => 'new'
map.resource :user_session
map.resources :users
map.root :controller =>"users", :action => "index"


La difference entre map.resources et map.resource est bien expliquée dans le guide Rails ; on fixe map.root sur un page simple, en attendant mieux.
Il reste à intégrer le mécanisme d’authentification dans le controller (et les prochains controller devront aussi avoir cette modification) :
class UsersController < ApplicationController
before_filter :require_user



4. Création des vues

C’est maintenant que haml et formtastique vont être utiles … rendez-vous sur leur doc respective pour plus de détail ou dans les commentaires :)
Création de la page de login app/view/user_sessions/new.html.haml:
#bloc_login
%h1 Red
- semantic_form_for @user_session, :url => user_session_path do |f|
- f.inputs do
= f.input :ident, :label => 'Login'
= f.input :password
= f.input :remember_me, :required => false, :as => :boolean
- f.buttons do
= f.commit_button " Login "


Création d’un bloc header qui sera present sur toutes nos pages (fichier app/view/shared/_head.html.haml à créer) :
#user_nav
= "( "+pluralize(User.logged_in.count, "user") + " currently logged in )"
- if current_user
= link_to current_user.ident, current_user
= link_to "Logout", logout_path
- else
= link_to "Register", signup_path
= link_to "Login", login_path
= link_to "Lost Password", "password_reset_path"


Et integration du partiel dans un fichier maquette unique pour l’ensemble du site : app/view/layout/application.html.haml (attention, les script/generate génèrent un layout associé au controller, donc il faut les supprimer sinon ils seront employés : le layout application n’est utilisé que si le layout du controleur est absent.)
!!!
%html
%head
%title Red
= stylesheet_link_tag 'scaffold'
= stylesheet_link_tag 'formtastic'
= stylesheet_link_tag 'red'
= stylesheet_link_tag 'login' if not current_user
= javascript_include_tag :defaults
%body
= render :partial => 'shared/head' if current_user
%p.flash_notice= flash[:notice]
= yield


Je vous livre toutes les vues User ce-dessous. A noter quelques astuces : en haml quand il faut poursuivre une commande sur plusieurs ligne, il faut ajouter une barre verticale | à la fin de toutes les lignes qui composent la commande ; toujours en haml, pour insérer du texte qui ne sera pas interprété (pratique pour tester vite fait un style css) il faut faire un bloc commençant par :plain. Par exemple :
...
%style{:type => "text/css"}
:plain
.imp{padding-left:20px;}
.imp.i1{background-image:url(exclamation.png);color:red;}
.imp.i2{background-image:url(information.png);}
%ol#event_list{:style=>'list-style:none'}
...

Pour les partial, il faut penser que le nom du fichier est égal à la variable passée au partial ... Ca sera plus clair dans un prochain exemple. Le nom du fichier est égal au nom du partial précédé d’un tiret bas : @user -> _user
Voici les vues :
app/views/users/index.html.haml
%h1 User
%h3 Liste User
%table.table_index
%thead
%tr
%th Nom
%th Prenom
%th Login
%th Email
%th
%tbody
- @users.each do |u|
%tr{ :class => cycle('even', 'odd')}
%td= u.username
%td= u.lastname
%td= u.ident
%td= u.email
%td
= link_to 'show', u
= link_to 'edit', edit_user_path(u)
= link_to 'delete', u, :confirm => 'Are you sure?', :method => :delete
%br
= link_to 'New user', new_user_path

app/views/users/show.html.haml
 %h1 User
%h3 Show User
- ['username','lastname','email','ident','login_count','last_request_at', |
'last_login_at','current_login_at','last_login_ip','current_login_ip'].each do |f| |
%p
%b= f.humanize
= @user[f]
%br
= link_to 'Edit', edit_user_path
|
= link_to 'Back', users_path

app/views/users/edit.html.haml
 %h1 Edit User
%h3{:style => 'border-bottom: 1px solid #700E27'} Edit User
#bloc_form
= render @user
%br
= link_to 'Show', @user
|
= link_to 'Back', users_path

app/views/users/new.html.haml
 %h1 User
%h3 New User
#bloc_form
= render @user
%br
= link_to 'Back', users_path

app/views/users/_user.html.haml
 - semantic_form_for @user do |f| 
= f.error_messages
- f.inputs do
= f.input :ident, :label => "Login", :input_html => {:size => 20}
= f.input :username, :label => "Nom"
= f.input :lastname, :label => "Prenom", :required => false
= f.input :email
= f.input :password, :required => false
= f.input :password_confirmation, :required => false
- f.buttons do
= f.commit_button


Les fichiers css vite fait : (/public/stylesheets/sass/red.sass) :NB : tous les fichiers *.sass contenus dans ce répertoire (configurable, voir la doc) sont transformés en fichier css automatiquement.
 body
:margin 0
:padding 0
#content
//:width 60%
:margin 20px
h3
:border-bottom 1px solid #700E27
#bloc_form
:margin-left 20px
input, select,textarea
:border 2px solid #DBCCB6
form.formtastic fieldset ol li
:display block
// -----------------head
#head
:background-color #700E27
:font-size 0.9em
:margin-bottom 10
:height 34px
:color white
#logo
:float left
:font-family 'Jokerman'
:font-size 2.2em
:padding 6px 0 0 10px
#user_nav
:float right
:padding 5px 20px 5px 0
#head * a
:font-weight bold
:padding 10px 3px 10px 3px
&:link
:color white
:text-decoration none
&:hover
:background-color #A00
:border-bottom 3px red solid
&:visited
:color white
:text-decoration none
// ------------------table des index
.table_index
:border 1px solid #DBCCB6
:border-spacing 0
:padding 0
thead
:background-color #A00
:margin 0
:padding 0
:color #FFF
:font-weight bold
:text-decoration none
th, td
:margin 0
:padding 2px
tr.odd
:background-color #EEE
tbody > tr:hover
:background-color #FFD

Et le fichier login.sass :
 // -----------------login
body
:background-color #700E27
:color white
#bloc_login
:width 600px
:margin 0 auto 0 auto
:padding-top 170px
h1
:font-family 'Jokerman'
:font-size 96pt
:text-align center
fieldset
:border 0
ol
:list-style none
li
:margin-bottom .8em
input
:width 40%
#user_session_submit
:width 100%
label
:display block
:text-align right
:width 40%
:float left
:padding-top .2em
:margin-right 20px


Et c’est le moment de tester (pour plus d’info sur rake faite >rake –T ):
>rake db :create
>rake db :migrate
>mongrel_rails start –p 80


Ca roule chez moi, mais j’ai pu louper un truc en écrivant le tutorial. N’hésitez pas à intervenir dans les commentaires.

TODO :
- configuration de l’envoi d’un email d’inscription
- perte de password





Read More......

jeudi 18 juin 2009

Initialisation de l'environnement


Cette première partie couvre l'installation complète de Ruby et de l'environnement de base (éditeurs, outils ligne de commande, ...) ainsi que la création du squelette Rails de l'application.






Installation de Ruby sous windows (avec One-click installer)

Vérifier que le path a bien été inclus :
>path
=> PATH = ... ;c:\ruby\bin; ...
>ruby -v
=> ruby 1.8.6 (2008-08-11 patchlevel 287) [i386-mswin32]
Mise à jour des gems :
>gem update system
>gem -v
=> 1.3.4
>gem update
Ajout d'une autre source pour les téléchargement de gem :
gem sources -a http://gems.github.com
Installation de Rails :
>gem install rails
>rails -v
=> Rails 2.3.2
Installation de SQLite : (base de donnée par défaut sur Rails, très simple pour travailler en développement)
>gem install --version 1.2.3 sqlite3-ruby
Attention, il ne faudra pas mettre à jour cette gem, car la version 1.2.4 n'est pas pour l'instant utilisable sous windows.

Pour éviter de mettre à jour toutes les gems, au lieu de faire habituellement :
>gem update
faites :
>gem outdated
=>RedCloth (4.2.0 > 4.2.1)
hoe (2.1.0 > 2.2.0)
justinfrench-formtastic (0.2.0 > 0.2.1)
rmagick (2.9.0 > 2.9.2)
ruby-opengl (0.60.0 > 0.60.1)
sqlite3-ruby (1.2.3 > 1.2.4)
et sélectionner alors juste les gems qu'il faut mettre à jour (rmagick est dans le même cas) :
>gem update RedCloth hoe justinfrench-formtastic
Installation du serveur Mongrel (meilleur que celui par défaut de Rails) :
>gem install mongrel
Installation de l'environnement de travail : là on peut faire compliqué en installant des IDE complets de développement sous windows (Aptana par exemple) mais je préfère une configuration légère (à la textmate) :
  • La console de commande sous DOS : Console (permet d'ouvrir plusieurs onglets, très pratique)
  • Le visualisateur SQLite (NB : on peut toujours passer par la ligne de commande avec >sqlite3 mabase.db3): SQLiteBrowser
  • Et l'éditeur : soit Intype soit RoRed (tous deux en phase de développement avec quelques bugs, mais suffisant pour travailler).
Pour Firefox, les extensions Firebug, Colorzilla, MeasureIt sont les principales.

La documentation reste primordiale pour débuter :
>gem server
vous permet d'accéder à http://localhost:8808 à toutes les documentations de vos gems installées, et sinon deux sources principales : les guides de Rails et l'API commentée.

Et enfin création de l'application Rails qui créera toute l'arborescence des fichiers :
>rails red
J'ai choisi un nom court : red (plus rapide quand on doit taper des lignes de commande sous console...)
Lancement du server pour vérification (à la racine de votre application) :
>mongrel_rails start -p 80
Le paramètre -p 80 indique de forcer l'écoute sur le port 80, pour accéder à l'application à l'adresse http://localhost/ au lieu de http://localhost:3000/, 3000 étant le port par défaut.

NB : on peut lancer plusieurs application rails en même temps, mais bien sur pas sur le même port, en se plaçant à chaque fois à la racine du projet, très utile pour tester différentes versions d'un projet (dans différents répertoires) ou d'autres projets pour s'en inspirer.

Voilà, ne reste plus qu'à ouvrir Intype et la Console et se positionner à la racine du projet pour commencer ...



Read More......