-
Notifications
You must be signed in to change notification settings - Fork 0
Backend Training: Non ActiveRecord Model
🔖 railscast-non-activerecord-model
, simple_form
Sometimes you need to create resources / models that are similar to ActiveRecord Models but do not need to save them to the database. This is the time when you need a Non-ActiveRecord Model that has ActiveRecord Model's functionalities but without the database table.
For our case, the subscription model is exactly like this since the records are saved directly on MailChimp not on our end.
To begin, we will create a subscription model with email, first_name, and last_name.
# app/models/subscriber.rb
class Subscriber
include ActiveModel::Model
attr_accessor :email, :first_name, :last_name
validates :email, presence: true, email_format: true
def initialize(attributes = {})
self.attributes = attributes
end
def persisted?
false
end
def new_record?
true
end
def create
valid?
# link to MailChimp here later
end
def self.create(attributes = {})
object = new(attributes)
object.create
end
alias save create
private
def attributes=(attributes)
attributes.each do |name, value|
send("#{name}=", value)
end
end
end
We try to mimic ActiveRecord as much as possible so it works like other models. Like you see in the above code, all the methods are standard ActiveRecord methods and we are overriding them for our purpose.
Now, let's test out the model in rails console.
$ rails c
irb(main):011:0> s = Subscriber.new
=> #<Subscriber:0x007fedad4b6228>
irb(main):012:0> s.valid?
=> false
irb(main):013:0> s.errors
=> #<ActiveModel::Errors:0x007feda3b03518 @base=#<Subscriber:0x007fedad4b6228 @validation_context=nil, @errors=#<ActiveModel::Errors:0x007feda3b03518 ...>>, @messages={:email=>["can't be blank", "format not valid"]}, @details={:email=>[{:error=>:blank}]}>
irb(main):014:0> s.email = 'johndoe.com'
=> "johndoe.com"
irb(main):015:0> s.valid?
=> false
irb(main):016:0> s.errors
=> #<ActiveModel::Errors:0x007feda3b03518 @base=#<Subscriber:0x007fedad4b6228 @validation_context=nil, @errors=#<ActiveModel::Errors:0x007feda3b03518 ...>, @email="johndoe.com">, @messages={:email=>["format not valid"]}, @details={}>
irb(main):017:0> s.email = '[email protected]'
=> "[email protected]"
irb(main):018:0> s.valid?
=> true
Great! The validations work just like other ActiveRecord Model.
Next, we can add this to our post page like on the mockup..
Let's add the subscriber model to our controller
class PostsController < ApplicationController
before_action :find_dates, :find_categories, :init_subscriber
......
private
......
def init_subscriber
@subscriber = Subscriber.new
end
end
Next, create a subscriber controller to handle the create action.
$ rails g controller subscribers
Go to the app/controllers/subscribers_controller.rb
and add...
# app/controllers/subscribers_controller.rb
class SubscribersController < ApplicationController
layout false
respond_to :js
def create
@subscriber = Subscriber.new subscriber_params
return unless @subscriber.save
@subscriber = Subscriber.new
flash.now[:success] = t('.success')
end
private
def subscriber_params
params.fetch(:subscriber, {}).permit(:email)
end
end
p.s. Here we are using ajax as well to handle the subscriber form
Add the routes for subscribers
# config/routes.rb
Rails.application.routes.draw do
......
resources :contacts, only: %i[new create]
constraints format: :js do
resources :subscribers, only: %i[create]
end
......
end
Next, create a form file for subscriber
# app/views/subscribers/_form.slim
= flash_messages
= simple_form_for @subscriber, remote: true do |f|
= f.input :email,
label: false
= f.button :button do
= fa_icon('chevron-right')
And add it to the app/views/posts/_aside.slim
# app/views/posts/_aside.slim
aside
.panel.panel-default
.panel-heading.text-uppercase
= t('.archives.title')
.....
.panel.panel-default
.panel-heading.text-uppercase
= t('.categories.title')
.....
.panel.panel-default
.panel-heading.text-uppercase
= t('.subscribers.title')
.panel-body
= render 'subscribers/form'
Add a response for subscriber create
# app/views/subscribers/create.js.erb
$('#new_subscriber').html("<%= j render('form') %>");
Add the translations
# config/locales/simple_form.en.yml
en:
simple_form:
......
placeholders:
contact:
......
subscriber:
email: Email Address*
hints:
subscriber:
email: Duis vehicula id mi et fringilla. Morbi a sapien nec orci.
# config/locales/en.yml
en:
......
posts:
......
aside:
archives:
title: Archives
past: All Past News
categories:
title: Categories
all: Uncategorised
subscribers:
title: Subscribe our newsletter
contacts:
......
subscribers:
create:
success: Thank you form subscribing!
Go to /posts
and you should see...
Submit the subscriber from without filling out the email and you should see...
Now, fill your email and submit the form and you should see...
Add controller specs
# spec/controllers/subscribers_controller_spec.rb
require 'rails_helper'
RSpec.describe SubscribersController, type: :controller do
describe 'Subscriber create Ajax' do
it 'renders create template' do
post :create, params: {}, format: :js
expect(response).to render_template('subscribers/create')
end
it 'renders create template' do
params = {
subscriber: {
email: Faker::Internet.email
}
}
post :create, params: params, format: :js
expect(response).to render_template('subscribers/create')
end
it 'returns success message' do
params = {
subscriber: {
email: Faker::Internet.email
}
}
post :create, params: params, format: :js
expect(flash[:success]).to be_present
end
end
end
Run specs for this file
$ rspec spec/controllers/subscribers_controller_spec.rb
...
Finished in 0.16602 seconds (files took 5.98 seconds to load)
3 examples, 0 failures
Add a factory
# spec/factories/subscriber.rb
FactoryBot.define do
factory :subscriber do
email { Faker::Internet.email }
end
end
Add model specs
# spec/models/subscriber_spec.rb
require 'rails_helper'
RSpec.describe Subscriber, type: :model do
it 'has a valid factory' do
expect(build(:subscriber)).to be_valid
end
describe 'Validations' do
it { should validate_presence_of(:email) }
it { should allow_value('[email protected]').for(:email) }
it { should allow_value('[email protected]').for(:email) }
it { should allow_value('[email protected]').for(:email) }
it { should_not allow_value('abc@xxx').for(:email) }
it { should_not allow_value('abc.com').for(:email) }
it { should_not allow_value('abc.email.com').for(:email) }
it { should_not allow_value('abc@email').for(:email) }
end
end
Run specs for model
$ rspec spec/models/subscriber_spec.rb
.........
Finished in 0.14755 seconds (files took 5.59 seconds to load)
9 examples, 0 failures
Next, for subscriber we are validating email format again so let's move email format examples to shared_examples
# spec/shared_examples/formats/email_format.rb
RSpec.shared_examples 'email format' do
it { should allow_value('[email protected]').for(:email) }
it { should allow_value('[email protected]').for(:email) }
it { should allow_value('[email protected]').for(:email) }
it { should_not allow_value('abc@xxx').for(:email) }
it { should_not allow_value('abc.com').for(:email) }
it { should_not allow_value('abc.email.com').for(:email) }
it { should_not allow_value('abc@email').for(:email) }
end
And replace both contact and subscriber model spec files
# spec/models/contact_spec.rb
require 'rails_helper'
RSpec.describe Contact, type: :model do
......
describe 'Validations' do
it { should validate_presence_of(:name) }
it { should validate_presence_of(:email) }
it { should validate_presence_of(:message) }
include_examples 'email format'
end
end
# spec/models/subscriber_spec.rb
require 'rails_helper'
RSpec.describe Contact, type: :model do
......
describe 'Validations' do
it { should validate_presence_of(:email) }
include_examples 'email format'
end
end
Run your model specs to see if it works.
$ rspec spec/models/
*...................................
Pending: (Failures listed here are expected and do not affect your suite's status)
1) AdminUser add some examples to (or delete) /Users/ilunglee/Work/rails/mock_project/spec/models/admin_user_spec.rb
# Not yet implemented
# ./spec/models/admin_user_spec.rb:4
Finished in 0.8897 seconds (files took 5.89 seconds to load)
36 examples, 0 failures, 1 pending
Great! Let's commit and move on.... p.s. fix all the rubocop errors and continue
$ git add -A
$ git commit -m 'Subscriber Form'