Rails Multi-step form - ruby-on-rails

I'm writing a quiz app with rails 5. I have got a multi-step form for question building.
Models:
class Mcq < ApplicationRecord
attr_accessor :option_count
has_many :options, dependent: :destroy
belongs_to :quiz
accepts_nested_attributes_for :options
validates :question_text, presence: true
end
class Option < ApplicationRecord
belongs_to :mcq, optional: true
validates :option_text, presence: true
end
Schema:
create_table "mcqs", force: :cascade do |t|
t.string "question_text"
t.boolean "required"
t.boolean "multiselect"
t.integer "quiz_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "options", force: :cascade do |t|
t.string "option_text"
t.integer "mcq_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
The first page is for question setup and has the following fields:
Option Count
Required (Yes / No)
No of options that can be selected (Single / Multiple)
The second page is for options and has the following fields:
Question Text
Nested Form for Options
Controller:
class McqsController < ApplicationController
def new
session[:current_step] ||= 'setup'
session[:mcq_params] ||= {}
#current_step = session[:current_step]
#quiz = Quiz.find(params[:quiz_id])
#mcq = Mcq.new(session[:mcq_params])
if session[:current_step] == 'options'
#option_count = session[:mcq_params]['option_count']
#option_count.times { #mcq.options.build }
end
end
def create
if params[:previous_button]
session[:current_step] = 'setup'
redirect_to new_quiz_mcq_path
elsif session[:current_step] == 'setup'
save_session(params[:mcq])
redirect_to new_quiz_mcq_path
elsif session[:current_step] == 'options'
#mcq = Mcq.new(whitelisted_mcq_params)
#mcq.quiz_id = params[:quiz_id]
#quiz = Quiz.find(params[:quiz_id])
if #mcq.save
session[:current_step] = session[:mcq_params] = nil
redirect_to quiz_new_question_path(#mcq.quiz_id)
else
#current_step = session[:current_step]
render :new
end
end
end
private
def whitelisted_mcq_params
params.require(:mcq)
.permit(:question_text, :multiselect, :required, options_attributes: [:option_text])
end
def save_session(mcq_params)
session[:mcq_params][:option_count] = mcq_params[:option_count].to_i
session[:mcq_params][:required] = mcq_params[:required]
session[:mcq_params][:multiselect] = mcq_params[:multiselect]
session[:current_step] = 'options'
end
end
The above solution works, but the code is messy and difficult to understand. I came across this railscasts episode which does something similar in a cleaner way. I've updated my code as follows:
class Mcq < ApplicationRecord
has_many :options, dependent: :destroy
belongs_to :quiz
attr_writer :current_step
attr_accessor :option_count
accepts_nested_attributes_for :options
validates :question_text, presence: true
def current_step
#current_step || steps.first
end
def steps
%w[setup options]
end
def next_step
self.current_step = steps[steps.index(current_step)+1]
end
def previous_step
self.current_step = steps[steps.index(current_step)-1]
end
def last_step?
current_step == steps.last
end
end
class McqsController < ApplicationController
def new
session[:mcq_params] ||= {}
#quiz = Quiz.find(params[:quiz_id])
#mcq = Mcq.new(session[:mcq_params])
#mcq.current_step = session[:mcq_step]
end
def create
#quiz = Quiz.find(params[:quiz_id])
session[:mcq_params].deep_merge!(params[:mcq]) if params[:mcq]
#mcq = Mcq.new(session[:mcq_params])
#option_count = session[:mcq_params]['option_count']
#option_count.times { #mcq.options.build }
#mcq.quiz_id = params[:quiz_id]
#mcq.current_step = session[:mcq_step]
if params[:previous_button]
#mcq.previous_step
elsif #mcq.last_step?
#mcq.save if #mcq.valid?
else
#mcq.next_step
end
session[:mcq_step] = #mcq.current_step
if #mcq.new_record?
render "new"
else
session[:mcq_step] = session[:mcq_params] = nil
redirect_to edit_quiz_path(#mcq.quiz_id)
end
end
end
But each time the second page is shown, the no of fields for options doubles or in case of invalid entry only the field for question_text is shown. How do I show the options correctly? Should I just go with my first solution? I'm new to rails and don't know much about the best practices.
Edited :
new.html.erb
<div class="sub-heading">Add a Multiple Choice Question:</div>
<%= render "mcq_#{#mcq.current_step}", quiz: #quiz, mcq: #mcq %>
_mcq_setup.html.erb
<div class="form-container">
<%= form_for [quiz, mcq] do |f| %>
<div class="form-row">
<div class="response-count">How many options should the question have?</div>
<%= f.select(:option_count, (2..5)) %>
</div>
<div class="form-row">
<div class="response-count">How many options can be selected?</div>
<div class="option">
<%= f.radio_button :multiselect, 'false', checked: true %>
<%= f.label :multiselect, 'Just One', value: 'false' %>
</div>
<div class="option">
<%= f.radio_button :multiselect, 'true' %>
<%= f.label :multiselect, 'Multiple', value: 'true' %>
</div>
</div>
<div class="form-row">
<div class="response-count">Is the question required?</div>
<div class="option">
<%= f.radio_button :required, 'true', checked: true %>
<%= f.label :required, 'Yes', value: 'true' %>
</div>
<div class="option">
<%= f.radio_button :required, 'false' %>
<%= f.label :required, 'No', value: 'false' %>
</div>
</div>
<%= f.submit "Continue to the Next Step" %>
<% end %>
</div>
_mcq_options.html.erb
<%= form_for [quiz, mcq] do |f| %>
<%= f.label :question_text, 'What is your question?' %>
<%= f.text_field :question_text %>
<%= f.fields_for :options do |option_fields| %>
<%= option_fields.label :option_text, "Option #{option_fields.options[:child_index] + 1}:" %>
<%= option_fields.text_field :option_text %>
<% end %>
<%= f.hidden_field :multiselect %>
<%= f.hidden_field :required %>
<%= f.submit "Add Question" %>
<%= f.submit "Back to previous step", name: 'previous_button' %>
<% end %>

You may look in direction of state_machine. By using that you can use your steps as states of state machine and use its ability to define validations that only active for given states (look here in state :first_gear, :second_gear do) so fields that required on second step will be not required on first. Also, it'll allow you to avoid complex checks for the step you currently on (because the state will be persisted in a model) and will be pretty easy to extend with more steps in a future.

Related

Rails 5: find_or_create_by not saving all the params

I have a form that should send info. One of the inputs is "empresa_id", and it's represented by a collection. I have checked in the server the form send the information I want. The thing is this field (only that one) is not saved when I run find_or_create_by. I've checked strong params and everything seem fine there.
SuscriptorsController
def create
#suscriptor = Suscriptor.new(suscriptor_params)
byebug #In this point #suscriptor.empresa_id has a correct value
if !#suscriptor.valid?
flash[:error] = "El email debe ser válido"
render 'new'
else
#suscriptor = Suscriptor.find_or_create_by(email: #suscriptor.email)
if #suscriptor.persisted?
if (#suscriptor.email_confirmation == true)
flash[:notice] = "Ya estás registrado/a"
redirect_to root_path
else
SuscriptorMailer.registration_confirmation(#suscriptor).deliver
end
else
flash[:error] = "Ha ocurrido un error. Contáctanos desde la sección contacto y explícanos"
render 'new'
end
end
private
def suscriptor_params
params.require(:suscriptor).permit(:email, :email_confirmation, :token_confirmation, :subtitle, :empresa_id)
end
Form view
<%= simple_form_for(#suscriptor) do |f| %>
<div class="input-group">
<div class="col-md-12">
<div class="form-group text">
<%= f.input :email, class: "form-control", placeholder: "tucorreo#email.com", required: true %>
<%= f.invisible_captcha :subtitle %>
<small id="emailHelp" class="form-text text-muted">Lo guardaremos y usaremos con cuidado.</small>
<%= f.input :empresa_id, collection: Empresa.all %>
</div>
<div class="input-group-append">
<%= f.submit "¡Hecho!", class: "btn btn-primary" %>
</div>
</div>
<% end %>
schema.rb
create_table "suscriptors", force: :cascade do |t|
t.string "email"
t.boolean "email_confirmation"
t.string "token_confirmation"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "empresa_id"
end
suscriptor.rb
class Suscriptor < ApplicationRecord
belongs_to :empresa, optional: true
before_create :confirmation_token
attr_accessor :subtitle
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 }, uniqueness: { case_sensitive: false }, format: { with: VALID_EMAIL_REGEX }
def confirmation_token
if self.token_confirmation.blank?
self.token_confirmation = SecureRandom.urlsafe_base64.to_s
end
end
end
In your find_or_create_by, your pass only the #suscriptor.email, and reassing the #suscriptor variable with the created suscriptor.
According API dock, you should pass a block to 'create with more parameters':
Suscriptor.find_or_create_by(email: #suscriptor.email) do |suscriptor|
suscriptor.empresa_id = #suscriptor.empresa_id
end
Be careful to not reassign #suscriptor variable before use the parameters.
You can read more about find_or_create_by in https://apidock.com/rails/v4.0.2/ActiveRecord/Relation/find_or_create_by
Hope this helps!

How to use form data in another MVC?

I am just at the very beginning of learning rails and ruby but am already stuck.
I have created a model, a view file and a controller with a form that allows the user to enter some data. Now I am working on another MVC that makes various calculations with these data and returns the result. My problem is: I simply don't understand how I get the data entered by the user to the model that makes the calculations. I know this is a very beginner's question, but well, looks like I can't solve it alone.
Entries Model: entry.rb
class Entry
include ActiveModel::Model
attr_accessor :icao, :string
attr_accessor :elevation, :integer
attr_accessor :qnh, :integer
attr_accessor :temperatur, :integer
attr_accessor :winddirection, :integer
attr_accessor :windspeed, :integer
attr_accessor :basedistance, :integer
validates_presence_of :icao
validates_presence_of :elevation
validates_presence_of :qnh
validates_presence_of :temperatur
validates_presence_of :winddirection
validates_presence_of :windspeed
validates_presence_of :basedistance
end
Entries View: new.html.erb
<% content_for :title do %>Flytical<% end %>
<h3>Airfield Conditions</h3>
<div class="form">
<%= simple_form_for #entry do |form| %>
<%= form.error_notification %>
<%= form.input :icao, label: 'Airfield ICAO', input_html: { maxlength: 4 }, autofocus: true%>
<%= form.input :elevation, label: 'Airfield Elevation', input_html: { maxlength: 4 }%>
<%= form.input :qnh, label: 'QNH', input_html: { maxlength: 4 }%>
<%= form.input :temperatur, label: 'Temperatur', input_html: { maxlength: 3 }%>
<%= form.input :winddirection, label: 'Wind Direction', input_html: { maxlength: 3 }%>
<%= form.input :windspeed, label: 'Wind Speed', input_html: { maxlength: 3 }%>
<%= form.input :basedistance, label: 'Base Takeoff Distance at MSL', input_html: { maxlength: 4 }%>
<%= form.button :submit, 'Submit', class: 'submit' %>
<% end %>
</div>
Entries Controller: entries_controller.rb
class EntriesController < ApplicationController
def new
#entry = Entry.new
end
def create
#entry = Entry.new(secure_params)
if #entry.valid?
flash[:notice] = "Entries for #{#entry.icao} received."
render :new
else
render :new
end
end
private
def secure_params
params.require(:entry).permit(:icao, :elevation, :qnh, :temperatur, :winddirection, :windspeed, :basedistance)
end
end
Now for the calculations. This is the Calculation model: calc.rb
You see I tried to get the data with #entries.xxx, but obviously that did not work
class Calc
include ActiveModel::Model
def pressalt
if #entry.qnh < 1013
pressalt = #entry.elevation + (1013 - #entry.qnh) * 30
else
pressalt = #entry.elevation
end
end
def densalt
if #entry.temperatur > 15
densalt = pressalt + (((120 * ((#entry.temperatur - (15 - #entry.elevation * 2 /1000)))
else
densalt = pressalt
end
end
end
Calculation view: new.html.erb
<% content_for :title do %>Flytical<% end %>
<h3>Results</h3>
<p> Test result <%= #densalt %> </p>
Calc controller: calcs_controller.rb
class CalcsController < ApplicationController
def create
#calc = Calc.new
end
end
And these are my routes:
Rails.application.routes.draw do
resources :entries, only: [:new, :create]
resources :calcs, only: [:new, :create]
root to: 'visitors#new'
end
Honestly, I have the impression that I am missing some basic, but major points - my hope is that I am going to learn it with this project.

5 form elements present, but only 4 are saving to the database. Rails

I am having a problem with my database. I am able to save all the elements of my form into the database but it is leaving out ":captcha" for some reason. :email, :first_name, :last_name and :user_message are all saving, but :captcha is not.
HTML form views/pages/index.html.erb
<%= form_for(#message) do |f| %>
<%= f.text_field :first_name, :class => "message_name_input message_input_default", :placeholder => " First Name" %>
<br><br>
<%= f.text_field :last_name, :class => "message_name_input message_input_default", :placeholder => " Last Name" %>
<br><br>
<%= f.text_field :email, :required => true, :class => "message_email_input message_input_default", :placeholder => " * Email" %>
<br><br>
<%= f.text_area :user_message, :required => true, :class => "message_user-message_input", :placeholder => " * Write a message" %><br><br>
<%= f.text_field :captcha, :required => true, :name => "captcha", :class => "message_input_default", :placeholder => " * #{#a} + #{#b} = ?" %><br><br>
<div id="RecaptchaField2"></div>
<%= f.submit "Send", :class => "messages_submit_button" %>
<% end %>
Pages Controller
class PagesController < ApplicationController
def index
#message = Message.new
#a = rand(9)
#b = rand(9)
session["sum"] = #a + #b
end
end
Messages Controller
class MessagesController < ApplicationController
def show
end
def new
#message = Message.new
end
def create
#message = Message.new(message_params)
if params["captcha"].to_i == session["sum"] && #message.save!
UserMailer.welcome_email(#message).deliver_now
redirect_to '/message_sent'
else
redirect_to '/'
end
end
private
def message_params
return params.require(:message).permit(:first_name, :last_name, :email, :user_message, :captcha)
end
end
Messages Migration
class CreateMessages < ActiveRecord::Migration
def change
create_table :messages do |t|
t.string :captcha
t.string :first_name
t.string :last_name
t.string :email
t.string :user_message
t.timestamps null: false
end
end
end
Schema
ActiveRecord::Schema.define(version: 20150822040444) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "messages", force: :cascade do |t|
t.string "captcha"
t.string "first_name"
t.string "last_name"
t.string "email"
t.string "user_message"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end
Routes
Rails.application.routes.draw do
resources :pages
resources :messages
resources :admins
get '/' => 'pages#index'
get '/new' => 'messages#new'
post '/message_sent' => 'messages#create'
get '/message_sent' => 'messages#show'
end
EDITED Attempted this code, but instead of saving 4 elements, it executes the "else" statement and redirects as if it is not being saved at all.
Messages Controller
class MessagesController < ApplicationController
def show
end
def new
#message = Message.new
end
def create
#message = Message.new(message_params)
if params[:message][:captcha].to_i == session["sum"] && #message.save!
UserMailer.welcome_email(#message).deliver_now
redirect_to '/message_sent'
else
redirect_to '/'
end
end
private
def message_params
return params.require(:message).permit(:first_name, :last_name, :email, :user_message, :captcha)
end
end
Remove name attribute from here:
<%= f.text_field :captcha, :required => true, :name => "captcha", :class => "message_input_default", :placeholder => " * #{#a} + #{#b} = ?" %><br><br>
It happens because name parameter is generated by rails itself, and it's responsible to structure your query. Thus this erb line:
<%= f.text_field :first_name %>
Will generate this html:
<input type="text" name="message[first_name]">
And when you submit form it will produce query like this
{ message: { first_name: 'value_of_input' } }
But you provided custom name that overridden default behaviour and produces requests like this:
{ captcha: 'captcha_val', message: { first_name: 'some_val1', last_name: 'some_val2', ... } }
Then you extract message params from params:
def message_params
params.
require(:message).
permit(:first_name, :last_name, :email, :user_message, :captcha)
end
Finally you create message with this hash:
{ first_name: .., last_name: .., email: .., user_message: .. }

How to only show public posts and not show private ones in Ruby on Rails?

I have a collection (or post if you prefer) that a User creates. The collection can be public or private (default is true in database). I want to let the User see all of their collections, but want anyone to be able to see ONLY collections set to be public.
What I mean is that when anyone (User or not) goes to view the Collection (ex. myapp.com/collections/some-collection), if it is public it will show, but it it is private it will not show and maybe render a "This Collection is Private" and redirect. I could use some help, thanks!
db/schema.rb
create_table "collections", force: :cascade do |t|
t.string "title"
t.integer "user_id"
t.boolean "display", default: true
t.text "description"
end
app/views/collections/show.html.erb
<div class="row">
<div class="col-lg-10">
<h2><%= best_in_place #collection, :title, as: :input %></h2>
<p><%= best_in_place #collection, :description, as: :textarea %></p>
<%= simple_form_for #collection do |f| %>
<%= f.input :display, as: :boolean, checked_value: true, unchecked_value: false %>
<%= f.button :submit %>
<% end %>
</div>
</div>
app/controllers/collections_controller.rb
def show
#links = #collection.follows
#collection = Collection.find_by_id(params[:id]) if params[:id].present?
if params[:id].blank?
#collections = Collection.user_collections(params.merge({"user_id" => current_user.id}))
end
end
app/models/collection.rb
class Collection < ActiveRecord::Base
belongs_to :user
has_many :follows
def links_count
follows.count
end
def add_link(url)
status, options, link = Link.link_exist?(url)
unless status
options = Utils.parse_page(url)
link = Link.create(options)
link.download_image!
end
link.follow_link({:collection_id => self.id, :user_id => user_id})
end
def self.user_collections(options)
conds = []
conds << " user_id = #{options["user_id"]}" if options["user_id"].present?
conds = conds.blank? ? [] : conds.join(" AND ")
Collection.where(conds).order("updated_at DESC").page(options["page"]).per(16)
end
end
before_filter :is_public?, only: [ :action_name]
def is_public?
unless #collection.display?
redirect_to :back
end
end
#collections = Collection.where('display', true) This should return all collections that can be displayed.

Index Sorted by Most Views

I'm having trouble with sorting an index of "pins" by most views. The view isn't showing any of the sorted "pins." When a user visits a pin, it stores the data in the 'visit_details' and 'visits' tables. In the model I have the functions to handle the sorting but I'm not sure if that is the problem or what's going on.
create_table "visit_details", force: true do |t|
t.integer "visit_id"
t.string "ip_address", limit: 15
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "visit_details", ["ip_address"], name: "index_visit_details_on_ip_address"
create_table "visits", force: true do |t|
t.integer "total_visits"
t.integer "unique_visits"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "pin_id"
end
add_index "visits", ["pin_id"], name: "index_visits_on_pin_id"
My controller:
class PinMostViewsController < ApplicationController
def index
#random_model = Pin.order('random()').first
#pins = Pin.top_viewed(params[:page], params[:date])
end
def pin_most_views_params
params.require(:pin).permit(:ip_address, :visit_id)
end
end
My pin.rb model stores the logic:
class Pin < ActiveRecord::Base
belongs_to :user
acts_as_commentable
has_attached_file :image, :styles => { :medium => "300X300>", :thumb => "100X100>" }
validates :image, presence: true
validates :description, presence: true
validates_attachment :image, content_type: { content_type: ["image/jpg", "image/jpeg", "image/png", "image/gif"] }
has_one :visit
validates :team, presence: true
validates :position, presence: true
def self.top_viewed(page, time_period)
case time_period
when "all_time"
Pin.all_time(page)
when "year"
Pin.order_most_viewed(page, 1.year.ago)
when "month"
Pin.order_most_viewed(page, 1.month.ago)
when "week"
Pin.order_most_viewed(page, 1.week.ago)
when "day"
Pin.order_most_viewed(page, 1.day.ago)
else
Pin.all_time(page)
end
end
def self.order_most_viewed(page, date)
visits, ids = {}, []
# Create a hash containing pins and their respective visit numbers
VisitDetail.includes(:visit).where('created_at >= ?', date).each do |visit_detail|
pin_id = visit_detail.visit.pin_id.to_s
if visits.has_key?(pin_id)
visits[pin_id] += 1
else
visits[pin_id] = 1
end
end
if visits.blank?
# Since no visits existed for this time period, we simply return an empty array
# which will display no results on the view page
[]
else
# Now we sort the pins from most views to least views
visits.sort_by{ |k,v| v }.reverse.each { |k, v| ids << k }
# With our array of ids, we get all of the pins in the particular order
Pin.page(page).per_page(30).where(id: ids).order_by_ids(ids)
end
end
# A nice query method that will sort by ids, used for the order_most_viewed class method above
def self.order_by_ids(ids)
order_by = ["case"]
ids.each_with_index.map do |id, index|
order_by << "WHEN id='#{id}' THEN #{index}"
end
order_by << "end"
order(order_by.join(" "))
end
def self.all_time(page)
Pin.includes(:visit)
.where('visits.id IS NOT NULL')
.order("visits.total_visits DESC")
.order("visits.total_visits > 0")
.page(page).per_page(30)
end
# Instance Methods
# ================
def image_remote_url=(url_value)
self.image = URI.parse(url_value) unless url_value.blank?
super
end
def previous
self.class.first(:conditions => ["id < ?", id], :order => "id desc")
end
def next
self.class.first(:conditions => ["id > ?", id], :order => "id asc")
end
end
And finally, my view (index of pins that are supposed to be sorted by most views)
<div id="pins" class="transitions-enabled">
<% #pins.each do |pin| %>
<div class="box panel panel-default">
<%= link_to image_tag(pin.image.url(:medium)), pin %>
<div class="panel-body">
<p><%= pin.description %></p>
<p><strong><%= pin.user.name if pin.user %></strong></p>
<% if current_user && pin.user == current_user %>
<div class="actions">
<%= link_to edit_pin_path(pin) do %>
<span class="glyphicon glyphicon-edit"></span>
Edit
<% end %>
<%= link_to pin, method: :delete, data: { confirm: 'Are you sure?' } do %>
<span class="glyphicon glyphicon-trash"></span>
Delete
<% end %>
</div>
<% end %>
</div>
</div>
<% end %>
</div>
Sorry, I can't comment yet. Did try tracing it?
You can use p right in the html to see what's going on there.
Also you can use logger.info in controller side.
Thus you will see, where you problem is hiding. When you see nil or []

Resources