An Interest In:
Web News this Week
- April 2, 2024
- April 1, 2024
- March 31, 2024
- March 30, 2024
- March 29, 2024
- March 28, 2024
- March 27, 2024
STI and multi attributes models in Rails
Once I had a task to collect data from different landing pages to a database. The task is challenging because each web form from a landing page has different inputs and data. I figured out how to write an elegant solution in a Rails application.
Preparation
First, we have to create a migration to store data in DB.
class CreateLandingForms < ActiveRecord::Migration def change create_table :landing_forms do |t| t.string :type t.jsonb :data t.timestamps end endend
The type is an important attribute for STI (single table inheritance) because we will have a few models (one model for each landing page) which use the same table in DB. Rails will automatically keep class name to the attribute.
The data is an attribute with JSONB data type. Each model has a unique amount of fields. We will store the information in the attribute as JSON.
Models
The main model looks like that. There is nothing special. Other models will be inherited from the main model.
# app/models/landing_form.rb# == Schema Information## Table name: landing_forms## id :bigint not null, primary key# data :jsonb# type :string# created_at :datetime not null# updated_at :datetime not null#class LandingForm < ApplicationRecordend
Lets create models to collect data from different web forms. For instance, from 2 landing pages: Christmass landing page and Black Fridays landing page.
Here is a model for Christmass landing page. Attributes are full name, phone number, city, state, address and a gift you would like to receive from Santa. :-) Method store_accessor
helps us to collect data to the data field and work with it such typical ActiveRecord attributes.
# app/models/landing_forms/christmas.rbmodule LandingForms class Christmas < LandingForm store_accessor :data, :full_name, :phone_number, :city, :state, :address, :gift validates :full_name, presence: true validates :phone_number, presence: true validates :city, presence: true validates :state, presence: true validates :address, presence: true validates :gift, presence: true def self.permitted_params [:full_name, :phone_number, :city, :state, :address, :gift] end endend
The next model - is a model for Black Fridays landing page. Attributes are first name, second name and email. If there is a requirement that email must be unique, we can add custom validation for email.
# app/models/landing_forms/black_friday.rbmodule LandingForms class BlackFriday < LandingForm store_accessor :data, :first_name, :last_name, :email scope :by_email, ->(email) { where(["data->>'email' IN (?)", email]) } validates :first_name, presence: true validates :last_name, presence: true validates :email, presence: true validate :unique_email def self.permitted_params [:first_name, :last_name, :email] end private def unique_email return if errors.size.positive? return if self.class.where(type: type).by_email(email).where.not(id: id).count.zero? errors.add(:email, I18n.t('landing_forms.email_already_taken')) end endend
You see there is nothing difficult to describe behaviour. In order to prove that the validation works we will write test cases. Fortunately store_accessor
works best with FactoryBot
.
# spec/factories/landing_form_factory.rbFactoryBot.define do # factory for one model factory :black_friday_form, class: LandingForms::BlackFriday do first_name { Faker::Name.first_name } last_name { Faker::Name.last_name } email { Faker::Internet.email } end # factory for another model factory :christmas_form, class: LandingForms::Christmas do full_name { Faker::Name.name_with_middle } phone_number { Faker::PhoneNumber.phone_number } city { Faker::Address.city } state { Faker::Address.state } address { Faker::Address.full_address } gift { Faker::Hipster.sentence } endend
And here are unit tests.
# spec/models/landing_form_spec.rbrequire 'rails_helper'RSpec.describe LandingForms::Christmas, type: :model do let(:item) { build(:christmas_form) } it 'works' do expect(item.save).to eq(true) endendRSpec.describe LandingForms::BlackFriday, type: :model do let(:item) { build(:black_friday_form) } it 'works' do expect(item.save).to eq(true) end describe 'unique email validation' do let!(:item1) { create(:black_friday_form) } context 'email is not the same' do let(:item2) { build(:black_friday_form) } it 'is valid' do expect(item2.valid?).to eq(true) expect(item2.save).to eq(true) end end context 'email is the same' do let(:item2) { build(:black_friday_form, email: item1.email) } it 'is not valid' do expect(item2.valid?).to eq(false) expect(item2.errors.attribute_names).to include(:email) expect(item2.errors[:email]).to include(I18n.t('landing_forms.email_already_taken')) end end endend
Controllers
In order to receive data from a client, we write endpoints in rails-router, one endpoint for each controller.
Rails.application.routes.draw donamespace :api do namespace :v1 do namespace :landing_forms do post :black_friday, to: 'black_friday#create' post :christmas, to: 'christmas#create' end end endend
A controller looks like that. Just one public method create
. We dont need anything more.
# app/controllers/api/v1/landing_forms/black_friday_controller.rbmodule Api module V1 module LandingForms class BlackFridayController < ApplicationController def create object = model_class.new(model_params) if object.valid? && object.save render json: { success: true } else render json: object.errors, status: :unprocessable_entity end end private def model_params params.permit(*model_class.permitted_params) end def model_class ::LandingForms::BlackFriday end end end endend
Let's write request test cases for the controller to prove that it works.
# spec/requests/api/v1/landing_forms/black_friday_spec.rbrequire 'rails_helper'RSpec.describe Api::V1::SubscriptionsController, type: :request do describe '#create' do subject { post '/api/v1/landing_forms/black_friday', params: params, as: :json } before { subject } context 'email validation' do let!(:item1) { create(:black_friday_form) } context 'email is unique' do let(:params) do { first_name: Faker::Name.first_name, last_name: Faker::Name.last_name, email: Faker::Internet.email } end it 'renders success' do expect(response).to have_http_status(:ok) expect(JSON.parse(response.body)['success']).to eq(true) end end context 'email is the same' do let(:params) do { first_name: Faker::Name.first_name, last_name: Faker::Name.last_name, email: item1.email } end it 'returns errors' do expect(response).to have_http_status(:unprocessable_entity) json = JSON.parse(response.body) expect(json.keys).to eq(['email']) expect(json['email']).to include(I18n.t('landing_forms.email_already_taken')) end end end endend
Voil! It works. Now its super easy to collect data from one more web form. We need:
- Create a new model and describe all attributes and validations
- Create a new endpoint in the route and a controller
Original Link: https://dev.to/kopylov_vlad/sti-and-multi-attributes-models-in-rails-12ce
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To