Affichage des articles dont le libellé est will_paginate. Afficher tous les articles
Affichage des articles dont le libellé est will_paginate. Afficher tous les articles

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......