Archives (mars 2009)



Upload de fichiers dans un champs de type BLOB d'une base Oracle



Ecrit par Anthony Heukmes le 31 mars 2009 10:49

0 commentaire



Les clients habitués aux mondes J2EE et Oracle sont souvent réticents à l'idée de passer à Ruby on Rails. Ils veulent avant tout s'assurer que ce nouvel outil permettra de réaliser au moins autant que l'ancien. Et c'est tout à fait normal.
Pour se faire, ils demandent régulièrement qu'une POC (Proof of Concept) soit développée afin de confirmer qu'une fonctionnalité donnée peut facilement être implémentée en Rails.

Lors d'une de mes dernières réunions, j'ai eu droit à la réflexion suivante : "Tu es sûr qu'il n'y aura pas de problèmes pour uploader des fichiers dans la base Oracle? J'avais moi aussi essayé un langage de script (PHP) et j'avais rencontré de nombreux problèmes".

Voici donc ma POC permettant de répondre à cette question.
J'ai spécifié Oracle dans le titre de cet article mais le code que vous allez découvrir n'a rien de spécifique à ce SGBD.

Nous avons tout d'abord besoin d'une table et d'un modèle pour stocker les données associées à notre fichier. Vous aurez également besoin d'un contrôleur et de différentes views.
Vous pouvez donc pour cela utiliser le script de génération fournit par Rails (scaffold) :


$ ./script/generate scaffold FileUpload name:string content_type:string content_size:integer content:binary


Voici mon fichier de migration :

class CreateFileUploads < ActiveRecord::Migration

def self.up
create_table :file_uploads do |t|
t.string :name
t.string :content_type
t.integer :content_size
t.binary :content
t.timestamps
end
end

def self.down
drop_table :file_uploads
end
end



Remarquez le type de la colonne "content" qui est binary. Cela générera un BLOB dans votre base de données lorsque vous effectuerez la migration :

$ rake db:migrate


Rien de particulier concernant le modèle :

class FileUpload < ActiveRecord::Base

end


Modifiez ensuite le formulaire de création (new.html.erb) afin qu'il ressemble à ceci :


<% form_for @file_upload, :html => { :multipart => true } do |f| %>
<%= f.error_messages %>
<p>
<%= f.label :content %><br />
<%= f.file_field :content %>
</p>
<p>
<%= f.submit 'Create' %>
</p>
<% end %>



Le formulaire a été défini comme étant multipart et le champ content est maintenant de type file_field.

Vous pouvez alors travailler sur la méthode create de votre contrôleur :

def create

@file_upload = FileUpload.new(params[:file_upload])
if @file_upload.valid?
@file_upload.name = params[:file_upload][:content].original_filename
@file_upload.content = params[:file_upload][:content].read
@file_upload.content_type = params[:file_upload][:content].content_type
@file_upload.content_size = params[:file_upload][:content].size
@file_upload.save
flash[:notice] = 'FileUpload was successfully created.'
redirect_to @file_upload
else
render :action => "new"
end
end


Rien de très compliqué, le paramètre params[:file_upload][:content] contient un objet de type ActionController::UploadedStringIO sur lequel vous pouvez effectuer différentes opérations telles que la lecture des données, la récupération du type de contenu, la taille, ... Toutes ces informations sont ensuite sauvées dans la base de données.

Dans la page show.html.erb, remplacez l'affichage du contenu par un lien de téléchargement :


<p>
<b>Content:</b>
<%= link_to "Download", :action => "download", :id => @file_upload.id %>
</p>


Créez enfin cette méthode download dans votre contrôleur :

def download

file = FileUpload.find(params[:id])
if file
send_data(file.content, :type => file.content_type , :filename => file.name)
else
flash[:error] = "Invalid file ID!"
redirect_to :action => "index"
end
end



Une fois de plus, c'est très simple. L'enregistrement de type FileUpload est récupéré dans la base de données grâce à son ID. Le fichier associé est alors retourné par l'intermédiaire de la méthode send_data.

Vous pouvez maintenant sauvegarder n'importe quel type de fichier dans votre base de données Oracle!

Ruby on Rails - Pourquoi inclure tous les helpers dans toutes les vues?



Ecrit par Anthony Heukmes le 25 mars 2009 21:55

0 commentaire



Dans l'application_helper.rb de mon projet actuel, j'ai différentes méthodes qui fournissent des comportements par défaut.
Ces méthodes sont donc communes à tous mes contrôleurs mais ceux-ci peuvent modifier le comportement de ces méthodes si nécessaire.
Pour cela, ils peuvent les redéfinir dans leur propre module helper.

Imaginons par exemple la méthode say_hi dans l'application_helper.rb :

def say_hi

"Hi from ApplicationController"
end"


Si j'appelle cette méthode dans les vues de mes contrôleurs Controller1 et Controller2, le texte "Hi from ApplicationController" sera correctement affiché.
Dans Controller1, je souhaite modifier ce message de bienvenue car le comportement par défaut ne me convient pas. Je peux donc redéfinir la méthode say_hi dans controller1_helper.rb :

def say_hi

"Hi from Controller1"
end"


Et tout fonctionne très bien.
J'effectue maintenant la même opération dans controller2_helper.rb :

def say_hi

"Hi from Controller2"
end"


Le message de bienvenue affiché dans les pages du Controller2 sera correct. Par contre, les pages du Controller1 n'afficheront plus "Hi from Controller1" mais "Hi from Controller2".
C'est un comportement plutôt innatendu, je ne m'attendais en effet pas à un tel résultat.

Ce problème est causé par la ligne suivante ajoutée automatiquement dans votre application_controller lors de la génération de votre projet Rails :

helper :all


L'intérêt de cette méthode est de rendre tous les helpers que vous avez définis disponibles dans toutes les vues de vos différents contrôleurs.
Pour que cela soit possible, cet appel va inclure toutes les méthodes de tous les helpers dans la vue courante.
Controller2 apparaît en dernier dans la liste des helpers inclus (ordre alphabétique) et c'est donc la méthode say_hi de ce dernier qui sera prise en compte.

Vous pouvez constater ce fonctionnement en ouvrant le fichier actionpack/lib/action_controller/helpers.rb :

def helper(*args, &block)

args.flatten.each do |arg|
case arg
when Module
add_template_helper(arg)
when :all
helper(all_application_helpers)
when String, Symbol
file_name = arg.to_s.underscore + '_helper'
class_name = file_name.camelize

begin
require_dependency(file_name)
rescue LoadError => load_error
requiree = / -- (.*?)(.rb)?$/.match(load_error.message).to_a[1]
if requiree == file_name
msg = "Missing helper file helpers/#{file_name}.rb"
raise LoadError.new(msg).copy_blame!(load_error)
else
raise
end
end
add_template_helper(class_name.constantize)
else
raise ArgumentError, "helper expects String, Symbol, or Module argument (was: #{args.inspect})"
end
end


def all_application_helpers

extract = /^#{Regexp.quote(helpers_dir)}\/?(.*)_helper.rb$/
Dir["#{helpers_dir}/**/*_helper.rb"].map { |file| file.sub extract, '\1' }
end


Lorsque la méthode helper est appelée avec le paramètre :all, celle-ci va se réinvoquer avec comme paramètre, le résultat de l'appel à all_application_helpers.
Cette méthode va retourner un tableau contenant les noms des helpers présents dans le dossier app/helpers (dans mon cas, 'application', 'controller1' et 'controller2').
Le nouvel appel à la méthode helper va alors parcourir chaque élément du tableau et inclure le helper correspondant.

J'ai donc pu facilement régler mon problème en supprimant cette ligne (helper :all) de mon application_controller.rb

Mais plus généralement, je me pose des questions quant à l'intérêt de cette méthode.
Pourquoi les méthodes du helper associé au controller2 devraient être accessibles dans mon controller1?
Je n'ai aucun problème contre l'existence de cette méthode mais c'est pour moi une erreur de l'inclure par défaut dans l'application_controller d'une nouvelle application Rails.

[Rails vu de l'intérieur] Les variables d'instance et les contrôleurs



Ecrit par Anthony Heukmes le 03 mars 2009 22:44

0 commentaire



Lors d'un de mes derniers projets, j'ai du mettre en place un contrôleur "générique" permettant de traiter des objets ActiveRecord différents.
Ainsi, un administrateur devait par exemple pouvoir valider des commentaires et des suggestions de liens en utilisant la même interface (/activations).

Ruby étant un langage dynamique de nature, cette solution a bien entendu été très simple à implémenter.

Les astuces suivantes m'ont cependant été utiles :

Le code suivant permet de récupérer dynamiquement une classe dont le nom n'est connu qu'à l'exécution :

klass_name = "Article"

klass = Kernel.const_get(klass_name)
klass.find(:all, :order => 'created_at DESC')



Il peut également être très intéressant de définir/récupérer des variables d'instance dynamiquement :

var_name = "article"

instance_variable_set("@#{var_name}", klass.find(params[:id]))
instance_variable_get("@#{var_name}")



A ce propos, savez-vous comment est-il possible que les variables d'instance que vous définissez dans vos contrôleurs soient automatiquement disponibles dans vos vues?
Dans la plupart des frameworks vous devez explicitement spécifier les variables que vous souhaitez mettre à disposition des vues.
Ainsi avec Java et Spring par exemple, vous devez créer une Map contenant vos variables et les passer en paramètre à la méthode render.
Avec Ruby on Rails c'est automatique et c'est tant mieux! Mais comment cela fonctionne-t-il?

Très simplement!

La classe ActionView::Base (actionpack/lib/action_view/base.rb) possède la méthode suivante :

def copy_ivars_from_controller

if @controller
variables = @controller.instance_variable_names
variables -= @controller.protected_instance_variables if @controller.respond_to?(:protected_instance_variables)
variables.each { |name| instance_variable_set(name, @controller.instance_variable_get(name)) }
end
end



... et celle-ci sera invoquée par la méthode render de votre vue.
Cette méthode permet de récupérer toutes les variables d'instance du contrôleur courant (sans les variables protégées) et de les créer dans votre vue en utilisant instance_variable_set.