jbbarth's corner

Ma version de Simplelog, disponible sur mon espace Github, supporte désormais l’upload d’images, utilisables ensuite dans les posts ou pages de son choix :

Pour que le tout fonctionne, il faut disposer sur son serveur d’une bibliothèque de traitement d’image pour Ruby. Personnellement j’ai choisi ImageScience pour sa simplicité, mais je viens de me rendre compte qu’il y avait des bugs sur les images PNG. Je vais voir si je peux passer à autre chose pour la suite.

To be continued ;-)

EDIT: je suis passé à RMagick, tout a l’air de marcher pas mal. D’ailleurs pour ma version de Simplelog, c’est mis comme processeur par défaut (pour le moment en dur dans la classe, qui sait peut-être bientôt en configurable) :

class Image < ActiveRecord::Base
 
  has_attachment :content_type => :image,
                  :storage => :file_system,
                  :max_size => 5.megabytes,
                  :resize_to => '740x400>',
                  :thumbnails => { :thumb => '100x100>' },
                  :path_prefix => 'public/assets',
                  :processor => 'Rmagick'
 
  validates_as_attachment
 
end
ActiveRecord::Base.logger = Logger.new(STDOUT)

(thanks)

Comments: 0 (view/add your own) Tags: rails, ruby, tech

Here we go :

gem install tzinfo builder memcache-client rack rack-test rack-mount erubis mail text-format thor bundler i18n
gem install rails --pre

Let’s give it a try ;)

Comments: 0 (view/add your own) Tags: rails, ruby, tech

On me titille de part et d’autre : Nicolas, Linux Mag (deux fois en 6 mois!), Damien et Damien, et des discussions par-ci par-là.

Donc c’est parti, j’essaie de me faire une appli sur CouchDB. Avec plein d’arrière pensées :

  • mieux maitriser l’outil et les concepts sous-jacents pour mieux comprendre (et pourquoi pas contribuer à) Chef
  • comparer ça à MongoDB (voir le railscast) ; si je bute trop, j’essaierai peut-être Mongo
  • en finir avec le gouffre conceptuel objet/relationnel ; j’espère que ça sera concluant de ce côté :)

Je me fais une petite base de connaissances parce que le besoin commence à être vraiment trop criant. J’ai des tonnes d’items non-lus et/ou à garder dans mon reader RSS, et j’ai vraiment besoin d’un outil pour organiser ça sous forme de tags (et si possible garder des copies locales des articles).

Pour simplifier le tout, je commence à partir sur du Rails 3 :

echo 'gem "couchrest"' >> Gemfile
bundle install

Des nouvelles dans les prochaines semaines, le projet sera comme d’hab’ sur github

Je travaille en ce moment sur des plugins Engines pour Redmine. Ces plugins me serviront au boulot, et permettront de laisser une situation un peu plus propre que les bidouillages actuels à mon départ. En particulier en avançant sur le plugin de gestion d’un datacenter (site et dépôt github), j’apprends énormément de choses sur le fonctionnement de Rails/Redmine/Engines, et j’entame donc une série d’articles sur ces découvertes. Ces articles supposent d’avoir déjà lu le tutoriel du site, et je repartirai souvent de cet exemple.

Cela donnera certainement lieu à des entrées dans le wiki redmine.org ou des suggestions dans les tickets. Et puis ça m’astreindra à publier un peu, comme le fait Eric dans ses Daily Refactor du core Redmine depuis 3 semaines et pour les mêmes raisons, et aussi suite à cet article de Damien.

Allons-y.

init.rb : on y ajoute la clé et la valeur par défaut du paramètre qu’on veut introduire, par exemple ici “boolean_parameter”. On précise également un partial qui permettra de gérer les paramètres du plugin :

  settings :default => { :boolean_parameter => true },
           :partial => 'settings/my_plugin'

app/views/settings/_my_plugin.html.erb : on place ici un formulaire pour gérer nos paramètres. Il sera automatiquement accessible dans la partie Administration > Plugins > lien “Configurer” sur votre plugin. “plugin_my_plugin” est à remplacer dans ce qui suit par “plugin_[nom de votre plugin]” :

<p>
  <label>Paramètre booléen</label> 
  <%= check_box_tag 'settings[boolean_parameter]', 1,
                    Setting[:plugin_my_plugin][:boolean_parameter] %>
</p>

Et voilà ! Ensuite, n’importe où dans votre plugin, vous pourrez utiliser :

Setting[:plugin_my_plugin][:boolean_parameter]

En réalité en mettant “1” comme deuxième paramètre, vous ne stockerez pas un booléen, mais “1” (coché) ou “nil” (décoché). Si vous souhaitez obtenir “true” ou “false” absolument, vous pouvez utiliser :

!!Setting[:plugin_my_plugin][:boolean_parameter]

A voir en vrai ici

Au boulot nous avons une instance Redmine qui tourne avec une base Sqlite3 pour nos tickets internes. Pratique, mais nous avons aussi développé une offre d’hébergement Redmine ouverte à la demande sur l’intranet, sous Mysql. D’où passage de l’instance Sqlite sous Mysql.

On ne peut bien sûr pas se contenter d’un export SQL de Sqlite à réimporter sous Mysql : ces deux moteurs ne respectent pas exactement la même syntaxe SQL, et ne stockent pas leurs types primitifs de la même manière (exemple: les booléens, stockés en “1/0” sous Mysql, et en “t/f” sous Sqlite).

C’est là qu’arrive yaml_db , une biblitothèque à installer comme une gem ou comme un plugin dans une appli Rails, qui permet de réaliser des exports ou imports de sa base sous un format neutre, YAML (wikipedia).

Pour une migration “one shot”, le plus simple sera de cloner la lib dans le répertoire plugins de vos applis et de suivre les instructions proposées dans le README :

cd /path/to/my/app
cd vendor/plugins
git clone http://github.com/ludicast/yaml_db.git
cd -
rake db:dump
#modifications éventuelles du fichier db/data.yml (chez nous l'appli change d'adresse, donc on a remplacé toutes les anciennes URLs)
#changement de database.yml
rake db:load

Un outil simple, comme on aime :)

EDIT: j’ai titré “déplacer une base Rails” car ce plugin fonctionne bien surtout avec une base ActiveRecord, ORM de Rails par défaut. Pour une base quelconque rien de garanti :)

Il peut arriver qu’une classe de Redmine ne se comporte pas exactement comme vous le voudriez, ou que vous souhaitiez lui ajouter des propriétés.

C’est décrit en anglais sur la page Plugin Internals / Extending the Redmine Core du wiki officiel, qui renvoie vers la lecture de certains plugins d’Eric Davis pour des exemples.

Petit apparté, je partage assez l’analyse selon laquelle il est quasi inutile de maintenir une API pour surcharger les modèles / controlleurs. Cela dit, parfois les méthodes sont extrêmement longues et/ou sujettes à de fréquents changements. Toute surcharge dans un plugin induit donc un risque pour les futures versions du core…

Retour à nos moutons : admettons qu’on veuille ajouter au modèle Issue une méthode d’instance whoami qui retournerait “Je suis le ticket #XXX”. Exemple bidon, c’est pour la science.

Si on applique ce que préconise Eric, ça donne quelque chose de ce genre :

#init.rb
require_dependency 'issue_patch'
Dispatcher.to_prepare do
  Issue.send(:include, IssuePatch) unless Issue.included_modules.include? IssuePatch
end


#lib/issue_patch.rb
require_dependency 'issue'

module IssuePatch
  def self.included(base)
    base.extend(ClassMethods)
    base.send(:include, InstanceMethods)
    base.class_eval do
      unloadable #permet de décharger la classe en mode dev
    end
  end
  
  #ici nos méthodes de classe
  module ClassMethods
  end
  
  #ici nos méthodes d'instance
  module InstanceMethods
    def whoami
      "Je suis le ticket ##{self.id}"
    end
  end    
end

Classique, mais comme diraient certains amis “on voit pas trop ce que ça fait”.

Personnellement je préfère réouvrir la classe Issue, et ça a l’air de marcher tout aussi bien (en dev et en prod) :

#init.rb
config.to_prepare do
  require_dependency 'issue_patch'
end


#lib/issue_patch.rb
require_dependency 'issue'

class Issue
  def whoami
    "Je suis le ticket ##{self.id}"
  end
end

Différences :

  1. utilisation de “config” au lieu de “Dispatcher” ; sans importance à mon avis. C’est discuté un peu ici.
  2. ré-ouverture de la classe plutôt qu’inclusion d’un module ; je trouve ça plus lisible pour ce coup-ci

Attention, je ne dis pas que ce que fait Eric fonctionne moins bien. Au contraire, c’est peut-être plus “propre”, mais n’étant pas un développeur confirmé, si je ne comprends pas au premier coup d’oeil ce que j’ai fait, j’ai plus de mal à maintenir mon code.

Au passage, c’est une mauvaise idée d’appeler son patch “lib/issue_patch.rb”. Si tout le monde fait ça, on ne pourra pas faire fonctionner 2 plugins qui patchent la même classe en même temps. Beurk. D’ailleurs, c’était le cas pour des plugins à moi, donc autant utiliser des noms a priori uniques : commit redmine_drafts/ec06b8

Redmine supporte à ce jour 37 langues. Si vous souhaitez diffuser votre plugin, c’est une bonne idée de respecter les mêmes conventions que le core, pour en faciliter les traductions, voire proposer plusieurs traductions de votre plugin directement. C’est ce que je fais par exemple pour mon plugin “Datacenter” que je livre en anglais et en français (voir la page de wiki française).

Pour cela, Redmine utilise l’internationalisation de Rails. Chaque mot ou groupe de mot qui doit être traduit est associé à une clé unique. Chaque langue a son fichier YAML dans le dossier config/locales/, et dans ce fichier on indique que telle clé correspond à telle chaine de caractères. Par exemple, plutôt que d’écrire “Mon super plugin” directement dans vos vus et helpers, vous allez lui associer une clé de votre choix, mettons text_my_super_plugin.

Dans la vue, vous pourrez utiliser le helper l() (un L minuscule) de cette façon :

<%= l(:text_my_super_plugin) %>

Ensuite vous devrez associer cette clé à sa valeur pour chaque langue. Pour le français, le fichier config/locales/fr.yml de votre plugin ressemblera à ça :

fr:
  text_my_super_plugin: Mon super plugin

Et vous pouvez traduire votre appli en anglais, en ajoutant un fichier config/locales/en.yml contenant :

en:
  text_my_super_plugin: My great plugin

Pour un texte accentué ou comportant des caractères spéciaux, il suffira de mettre la chaine entre quotes pour éviter toute confusion lors de l’analyse du fichier. Attention à ce que votre fichier reste bien en UTF8 tout de même.

Si la traduction n’existe pas (fichier de langue manquant ou clé inexistante dans la langue de l’utilisateur), Redmine affichera une erreur. C’est la que le helper l_or_humanize peut être utile :

<%= l_or_humanize(:super_plugin) %>

Si la clé existe, elle sera remplacée par sa traduction. Si non, Rails tentera d’en faire une chaine pour humain (remplacement des underscores par des espaces, majuscule à la première lettre, etc.). En l’occurrence Super plugin.

Pour les affichages de dates, heures, temps ou intervalles de temps, il existe des helpers beaucoup plus évolués que ceux présentés ci-dessus. Ils sont définis dans lib/redmine/i18n.rb. En voici une liste, ainsi que comment les tester dans une console Rails :

% ruby script/console
Loading production environment (Rails 2.3.5)
>> include Redmine::I18n
=> Object
>> set_language_if_valid('fr')
=> :fr
>> l_hours(5)
=> "5.00 heures"
>> format_date(Time.now)
=> "26/04/2010"
>> format_time(Time.now)
=> "26/04/2010 19:55"
>> day_name(1)
=> "lundi"
>> month_name(3)
=> "mars"

A des fins de test, le helper ll() permet de préciser d’abord la locale avant la clé et ainsi de tester une clé dans une locale particulière :

>> ll("fi", :field_mail)
=> "Sähköposti"

Dernière chose, il est possible d’utiliser des variables dans vos fichiers de langue. Ils seront interpolés lors du rendu de la vue. Si vous n’avez qu’une variable à mettre, vous pouvez utiliser le nom “value” et passer la valeur dans votre vue directement en 2e argument de votre l(). Si vous avez 2 variables ou plus, il faut leur donner un nom et passer un hash en 2e argument de l() dans votre vue. Evidemment ces valeurs peuvent elles-même faire appel à vos traductions pour éviter de dupliquer des traductions.

Un exemple vaut mieux qu’un long discours. Avec ce fichier de langue :

fr:
  label_draft_saved_time: "Brouillon sauvegardé à {{value}}"
  label_draft_pending: "Brouillon en attente, sauvegardé il y a {{time}} : {{restore}} ou {{delete}}"
  label_draft_restore: "restaurer"
  label_draft_delete: "supprimer"

Je peux faire appel à ceci dans mes vues (les valeurs de temps sont bidon) :

<%= l(:label_draft_saved_time, format_time(Time.now)) %>
<%= l(:label_draft_pending, {:time => format_time(Time.now),
                             :restore => l(:label_draft_restore),
                             :delete => l(:label_draft_delete)}) %>

J’essaierai de documenter tout ça en anglais dans le wiki Redmine un de ces 4.