vluchtige ActiveRecord objecten

Gepubliceerd op: 26.IV.2006 09:32 CEST
Categorieën: active_form, rails, ruby

Ik ben een simpele web applicatie met rails aan het bouwen. Niets bijzonders; wat berichtjes, een gastenboek en wat formulieren. Deze laatste formulieren hoeven niet opgeslagen te worden maar moeten verstuurd worden via e-mail. Dat versturen is geen probleem; recht toe recht aan ActiveMailer. De uitdaging zit hem in het valideren van deze formulieren.

Uitdaging is misschien een beetje sterk uitgedrukt maar als je net het beheer van berichten met ActiveRecord hebt gebouwd, is het een beetje jammer als je dan toch weer dingen gaat schrijven als:

if params[:email].nil? || !EMAIL_PATTERN.match(params[:email])
  @errors[:email] = 'E-mail adres moet correct ingevuld worden.'
end

Neeh, daar pas ik voor! Ik wil gewoon form.valid? kunnen vragen zoals bij ActiveRecord objecten. Maar hoe doe je dat?

ActiveRecord classes vragen aan de database welke kolommen er in een record zitten. Omdat het type object waar ik mee aan de slag wil helemaal geen records opslaat wil ik dus ook geen tabel aanmaken. Op de Rails Weenie site vond ik een hint voor m’n oplossing. Door alleen self.columns te overschrijven kan je het standaard gedrag van AR overschrijven. Onze eigen implementatie moet dan alleen zelf zorgen dat self.columns een lijst column definities terug geeft.

class MyForm < ActiveRecord::Base
  def self.columns
    [
      ActiveRecord::ConnectionAdapters::Column.new('email', nil),
      ActiveRecord::ConnectionAdapters::Column.new('message', nil)
    ]
  end
  
  validates_presence_of :email, :message
end

Dit werkt! Zolang we natuurlijk geen form.save proberen natuurlijk. Maar de aanpak in de eerder genoemde hint is een stuk eleganter. Het kan natuurlijk nog beter:

class ActiveForm < ActiveRecord::Base
  def self.columns; @columns ||= []; end
    
  def self.column(name, sql_type = nil, default = nil, null = true)
    columns << ActiveRecord::ConnectionAdapters::Column.new(
        name.to_s, default, sql_type.to_s, null)
  end
end

Hiermee kunnen we MyForm als volgt schrijven:

class MyForm < ActiveForm
  %w{email message}.each { |c| column c }
  
  validates_presence_of :email, :message
end

en m’n controller actie:

def feedback
  @form = MyForm.new(params[:form])
  if request.post? && @form.valid?
    MyMailer.deliver_feedback(@form)
    redirect_to :action => 'thanks'
  end
end

Auw, ik trek krom van geluk!

Anoniempje @ ongeveer 1 uur

hmmm ik tel drie regels in je aller eerste voorbeeld en een heleboel ingewikkelde regels in je uiteindelijke oplossing. Oke DRY maar aangezien je verder geen Repeat hebt volgens mij is de eerste oplossing misschien wel zo simpel.

Danny @ ongeveer 4 uur

In de User class die je bij acts_as_authenticated meekrijgt, wordt een extra column gewoon als attribute gedefinieerd:

attr_accessor :password

waarna er ook validaties op uitgevoerd kunnen worden:

validates_presence_of     :password
validates_length_of       :password, :within => 5..40
validates_confirmation_of :password

Is dat iets anders??

Rodney @ ongeveer 13 uur

je kunt met attr_accessor ‘pseudo’/virtuele kolommen toevoegen aan je model
waarna je gewoon de normale validations kunt gebruiken. Zoiets:


class MyForm < ActiveForm
  attr_accessor :email
  validates_presence_of :email
end

Remco @ ongeveer 14 uur

Je kan attr_accessor gebruiken en self.columns een lege lijst column terug laten geven:


class MyForm < ActiveRecord::Base
  def self.columns; []; end
  attr_accessor :email, :message
  validates_presence_of :email, :message
end

Validatie werkt maar je object gedraag zich niet helemaal als een AR object omdat je de columns lijst niet hebt. De error_messages_for helper methode gebruikt bijvoorbeeld deze lijst om errors “op de juiste” volgorde te tonen. Deze helper toont voor de bovenstaande class nooit validatie fouten. Daarnaast verlies je de mogelijkheid om MyForm automagisch op te bouwen in je view mbv de columns lijst.

Anoniempje @ ongeveer 15 uur

En de mailer in Rails heet ActionMailer ipv ActiveMailer ;)

Matthijs Langenberg @ 1 dag

Interessante post!
Ik ben op dit moment met hetzelfde bezig, en heb dit ook op zo’n beetje dezelfde manier geimplementeerd. Je gebruikt toch wel vaak een support / contact formulier in een applicatie.
Ik vraag me trouwens af of er geen nettere manier gebruikt kan worden. Het zou mogelijk zijn om een plugin te maken en dus gewoon daar van te erven.

Remco @ 1 dag

Ik heb de ActiveForm.rb in de lib directory gezet en m’n form objecten zijn nu redelijk netjes. Zie m’n code snippet voor de gedocumenteerde code.

Daniel @ 2 maanden

Wat je beter kan doen is gewoon een in memory sqlite table te maken :


class Mailafriend < ActiveRecord::Base
   unless const_defined? :SQLITE_SETUP
     SQLITE_SETUP = true
     self.establish_connection(:adapter => 'sqlite', :database => ':memory:')
     conn = self.connection
     conn.create_table(self.table_name, :primary_key => self.primary_key) do |t|
       t.column :from_name, :string
       t.column :from_email, :string
       t.column :to_name, :string
       t.column :to_email, :string
       t.column :message, :string
     end
   end

attr_accessible :from_name attr_accessible :from_email attr_accessible :to_name attr_accessible :to_email attr_accessible :message validates_presence_of :from_name, :message => “U dient een naam van de afzender in te vullen”.t validates_presence_of :from_email, :message => “U dient een email-adres van de afzender in te vullen”.t validates_presence_of :to_name, :message => “U dient een naam van de ontvanger in te vullen”.t validates_presence_of :to_email, :message => “U dient een email-adres van de ontvanger in te vullen”.t

end

Remco @ 2 maanden

Cool! Wel jammer dat SQLite dan ook geinstalleerd moet worden. Daar wordt m’n klant niet echt gelukkig van.

Daniel @ 2 maanden

Over je hosting weet ik niks, maar als je op linux servers, of freebsd zit.
Is sqlite geen enkel probleem. En de installatie stelt helemaal niks voor.
Die is voor de meeste hostingproviders een stuk minder erg als rails :)