The Caribbean Weblog

"This blog is continuing @ http://christophemaximin.com "

Aller au contenu | Aller au menu | Aller à la recherche

jeudi 21 juin 2007

Rails : YAML et ActiveRecord (et SQL plus généralement)

S'est posé à moi le problème de transformer le contenu du fichier YAML lib/places.yml suivant en lignes SQL représentatives :

- USA:
  - California:
    - Los Angeles Area
    - San Diego Area
- Europe:
  - France:
    - Ile-de-France
    - Aquitaine:
      - Gironde:
        - Bordeaux
    - Alsace
  - Poland:
    - Mazowieckie:
      - Warszawa

..., fichier yaml qui devient :

+----+------------------+-----------+
| id | name             | parent_id |
+----+------------------+-----------+
|  1 | USA              |         0 | 
|  2 | California       |         1 | 
|  3 | Los Angeles Area |         2 | 
|  4 | San Diego Area   |         2 | 
|  5 | Europe           |         0 | 
|  6 | France           |         5 | 
|  7 | Ile-de-France    |         6 | 
|  8 | Aquitaine        |         6 | 
|  9 | Gironde          |         8 | 
| 10 | Bordeaux         |         9 | 
| 11 | Alsace           |         6 | 
| 12 | Poland           |         5 | 
| 13 | Mazowieckie      |        12 | 
| 14 | Warszawa         |        13 | 
+----+------------------+-----------+

..., contenu qui sera facilement utilisable, notamment avec acts_as_tree.

Voici la (très petite) classe que j'ai créé pour l'occasion (lib/yaml_to_ar.rb):

 require 'yaml'
 class YAML_to_AR


   def initialize(file)
     @data = File.open(file) { |yf| YAML::load( yf ) }
   end


   def insert(with_class, parent_id = 0, data = @data)
     if data.is_a?(Array)
       data.each do |value|
         insert(with_class, parent_id, value)
       end
     elsif data.is_a?(Hash)
       data.each do |key, value|
         new_line = with_class.create(:name => key, :parent_id => parent_id)
         insert(with_class, new_line.id, value)
       end
     elsif data.is_a?(String)
        with_class.create(:name => data, :parent_id => parent_id)
     end
   end


 end

Classe qui s'utilise simplement dans une migration, comme ceci par exemple (@ db/migrate/003_create_places.rb) :

 class CreatePlaces < ActiveRecord::Migration
   def self.up
     create_table :places do |t|
       t.column :name, :string
       t.column :parent_id, :integer
     end

     require 'lib/yaml_to_ar'
     loading = YAML_to_AR.new('lib/places.yml')
     loading.insert(Place)

   end

   def self.down
     drop_table :places
   end
 end

Ensuite, peut se poser la question de rajouter une méthode qui permette de modifier la table avec un fichier YAML du même style, se basant sur une sorte de "diff" entre les deux, mais vu que le problème ne s'est jamais présenté...

vendredi 15 juin 2007

Media Manager / Files Handler

Depuis que je me suis mis au Ruby on Rails, et donc initié au modèle MVC, j'ai toujours été stupéfait de voir un grand nombre de développeurs "gérer" les fichiers externes, envoyés par les internautes, dans leur controller ou leur model.

La logique même de séparation concepts différents aurait dû, selon moi, les inciter à créer des "medias manager", ou sortes de classes/modules pour gérer ces fichiers externes, de leur naissance à leur destruction.

N'ayant pas trouvé ce que je cherchais, j'ai créé il y a quelques mois un couple FileHandler/ImageHandler que j'ai étendu par la suite pour gérer les sons et vidéos. N'ayant depuis jamais encore trouvé de meilleure solution, j'aimerais bien savoir ce que vous en pensez. Voici donc une courte présentation de tout cela :
media manager rails
Et voici un exemple d'utilisation concret, une fois que les fichiers ont bien été 'required' comme il se doit :
in the view (app/views/photos/new.rhtml) :

<% form_for(:photo, :url => photos_path, :html => {:multipart => true}) do |f| %>
  <%= error_messages_for(:photo) %>
  <%=mff 'Sélectionnez le fichier à envoyer', file_field_tag(:file) %>
  <%=mff 'Description (facultatif)', f.text_area(:description) %>
  <p class="submit"><%= submit_tag('Envoyer cette photo »»') %></p>
<% end %>

in the controller (app/controllers/photos_controller.rb) :

class PhotosController < ApplicationController
#[...]
  def create
    @photo = Photo.create(params[:photo])
    @photo.store(params[:file])
    if @photo.save
      flash[:notice] = "Nouvelle photo enregistrée."
      redirect_to :action => 'new'
    else
      @photo.destroy
      render :action => 'new'
    end
  end
#[...]
end

in the model (app/models/photo.rb) :

 class Photo < ActiveRecord::Base
   before_destroy { |record| ImageHandler.delete_with_thumbnails(record.file_name) }

   def store(iostream)
     image = ImageHandler.new(iostream)
     begin
       image.store(self.id) # son nom de fichier sera son id + extension
     rescue
       @erreurs = $!.to_s # si une exception remonte l'ImageHandler ou du FileHandler, on l'ajoute à la liste d'erreurs de validations ActiveRecord
     else
       image.make_thumbnails 100, 150, 200, 250, 300 # chaque nombre représente le width d'un thumbnail de l'image qui sera fait
       update_attribute :file_name, image.raw_name # on rentre son nom dans la bdd
     end
   end

  protected
   def validate
     errors.add_to_base(@erreurs) if defined?(@erreurs) # l'ajout à la base se fait ici en réalité
   end
 end

Pages: