I am following Michael Hartl's Rails Tutorial and have completed the part about creating microposts. I was wondering if anyone have an idea about how to make the micropost form responsive to a hyperlink. For example, when a user types in "Visit our HTML tutorial" in the micropost, I want the link to active. Any help would be appreciated.
micropost_controller.rb
class MicropostsController < ApplicationController
before_action :signed_in_user, only: [:create, :destroy]
before_action :correct_user, only: :destroy
def create
#micropost = current_user.microposts.build(micropost_params)
if #micropost.save
flash[:success] = "Micropost created!"
redirect_to root_url
else
#feed_items = []
render 'static_pages/home'
end
end
def destroy
#micropost.destroy
redirect_to root_url
end
private
def micropost_params
params.require(:micropost).permit(:html)
end
def correct_user
#micropost = current_user.microposts.find_by(id: params[:id])
redirect_to root_url if #micropost.nil?
end
end
micropost.rb
class Micropost < ActiveRecord::Base
belongs_to :user
default_scope -> { order('created_at DESC') } validates :content,
presence: true, length: { maximum: 140 } validates :user_id,
presence: true end
...
end
micropost_form.html.erb
<%= form_for(#micropost) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class="field">
<%= f.text_area :content, placeholder: "Compose new micropost..." %>
</div>
<%= f.submit "Post", class: "btn btn-large btn-primary" %>
<% end %>
You can use the sanitize helper method and pass in the anchor (a) tag as the only allowable tag. You don't use it when they create the post, you use it when you are showing the micropost in the view
app/views/microposts/show.html.erb
<%= sanitize micropost.content, tags: ['a'] %>
(I don't know exactly how you are showing the content of a micropost, but this should give you an idea)
This is safer than other options like html_safe because you can actually control which html tags you will allow the user to be able to input.
Related
I feel like this should be an easy thing to do in Rails, but all of the examples of nested forms in Rails do not take into account the fact that most nested forms also need to pass the current_user when creating new objects through a nested form.
The only way I can get this to work at the moment is by passing a hidden field such as <%= form.hidden_field :user_id, value: current_user.id %>.
For my specific example, I have a model called "Result" that has many "Lessons" and I'd like to create new lessons through the Result form without passing a hidden :user_id.
This seems unsafe because someone could edit that hidden field in the browser and then submit the form thus associating the submission with a different user. The current_user.id seems like the type of thing you don't want to embed in the html as a hidden field.
So how do you create the association between the nested objects and the current_user without putting that hidden field in the form?
FYI, I'm using the GoRails nested form with stimulus style javascript to add and remove lessons from the result form. (Here's the source code for that example.) Here are the relevant parts of my code:
models/result.rb
class Result < ApplicationRecord
belongs_to :user
has_many :lessons, inverse_of: :result
accepts_nested_attributes_for :lessons, reject_if: :all_blank, allow_destroy: true
end
models/lesson.rb
class Lesson < ApplicationRecord
belongs_to :user
belongs_to :result
end
controllers/results_controller.rb
class ResultsController < ApplicationController
before_action :set_result, only: [:show, :edit, :update, :destroy]
def new
#result = Result.new
#result.lessons.new
end
def create
#result = current_user.results.new(result_params)
respond_to do |format|
if #result.save
format.html { redirect_to #result, notice: 'Result was successfully created.' }
else
format.html { render :new }
end
end
end
private
def set_result
#result = Result.find(params[:id])
end
def result_params
params.require(:result).permit(:prediction_id, :post_mortem, :correct,
lessons_attributes: [:user_id, :id, :summary, :_destroy])
end
end
controllers/lessons_controller.rb
class LessonsController < ApplicationController
before_action :set_lesson, only: [:show, :edit, :update, :destroy]
# GET /lessons/new
def new
#lesson = Lesson.new
end
def create
#lesson = current_user.lessons.new(lesson_params)
respond_to do |format|
if #lesson.save
format.html { redirect_to #lesson, notice: 'Lesson was successfully created.' }
else
format.html { render :new }
end
end
end
private
def set_lesson
#lesson = Lesson.find(params[:id])
end
def lesson_params
params.require(:lesson).permit(:result_id, :summary)
end
end
views/results/_form.html.erb
<%= form_with(model: result, local: true) do |form| %>
<h3>Lessons</h3>
<div data-controller="nested-form">
<template data-target="nested-form.template">
<%= form.fields_for :lessons, Lesson.new, child_index: 'NEW_RECORD' do |lesson| %>
<%= render "lesson_fields", form: lesson %>
<% end %>
</template>
<%= form.fields_for :lessons do |lesson| %>
<%= render "lesson_fields", form: lesson %>
<% end %>
<div class="pt-4" data-target="nested-form.links">
<%= link_to "Add Lesson", "#",
data: { action: "click->nested-form#add_association" } %>
</div>
</div>
<div class="form-submit">
<%= form.submit "Save" %>
</div>
<% end %>
views/results/_lesson_fields.html.erb
<%= content_tag :div, class: "nested-fields", data: { new_record: form.object.new_record? } do %>
# This hidden field seems unsafe!
<%= form.hidden_field :user_id, value: current_user.id %>
<div class="pb-8">
<%= form.text_area :summary %>
<%= link_to "Remove", "#",
data: { action: "click->nested-form#remove_association" } %>
</div>
<%= form.hidden_field :_destroy %>
<% end %>
I'm sure this is a common problem in Rails but I can't find any tutorials online that have the user_id as a part of the nested fields example. Any help is much appreciated!
Personally, since setting the current_user id is something the controller should care about, I would iterate over all the lessons and set the user_id value there.
def create
#result = current_user.results.new(result_params)
#result.lessons.each do |lesson|
lesson.user ||= current_user if lesson.new_record?
end
... the rest ...
Having a hidden field is a security risk, someone could edit it. I also don't like changing the params hash.
I don't think there is a great way to handle this automatically outside of the view. You would either have to inject the value unto the params or possible have a use default on the user association in Lesson that sets it from the Record's user (belongs_to :user, default: -> { result.user }). In these scenarios, I generally move outside of the default Rails flow and use a PORO, Form Object, service object, etc.
build form like this
<%= form.fields_for :lessons, lesson_for_form(current_user.id) do |lesson| %>
<%= render "lesson_fields", form: lesson %>
<% end %>
remove hidden user_id field you have added
update your result.rb file
class Result < ApplicationRecord
def lesson_for_form(user_id)
collection = lessons.where(user_id: user_id)
collection.any? ? collection : lessons.build(user_id: user_id)
end
end
I have done a phone number verification via Twilio, but I can't find a way how to implement a feature that sends pin code again (if user didn't received it) but also does it not more that 3 times (so users couldn't keep sending codes over and over again). Also, my code looks a bit anti-pattern, so feel free to suggest a better implementation.
When Devise User registers itself, I send him to create a Profile that belongs_to User. Profile holds all user info (and phone number). Here is the form:
<%= form_for #profile, remote: true do |f| %>
<%= f.label 'Your name' %><br />
<%= f.text_field :first_name, autofocus: true, class: 'form-control' %>
<%= f.label 'Phone number' %><br />
<%= f.text_field :phone, class: 'form-control' %>
</br>
<div id="hideAfterSubmit">
<%= f.submit 'Save', class: 'btn btn-lg btn-primary btn-block' %>
</div>
<% end %>
<div id="verify-pin">
<h3>Enter your PIN</h3>
<%= form_tag profiles_verify_path, remote: true do |f| %>
<div class="form-group">
<%= text_field_tag :pin %>
</div>
<%= submit_tag "Verify PIN", class: "btn btn-primary" %>
<% end %>
</div>
<div id="status-box" class="alert alert-success">
<p id="status-message">Status: Haven’t done anything yet</p>
</div>
#verify-pin and #status-box are display: none. I unhide them with responding create.js.erb.
Create action:
def create
if user_signed_in? && current_user.profile
redirect_to profile_path(current_user), notice: 'Jūs jau sukūrėte paskyrą'
else
#profile = Profile.new(profile_params)
#phone_number = params[:profile][:phone]
#profile.user_id = current_user.id
SmsTool.generate_pin
SmsTool.send_pin(phone_number: #phone_number)
if #profile.save
respond_to do |format|
format.js
end
else
render :new
end
end
end
So at this point profile been created, saved and pin code generated and sent to phone number that user just added.
SmsTool:
def self.generate_pin
##pin = rand(0000..9999).to_s.rjust(4, "0")
puts "#{##pin}, Generated"
end
def self.send_pin(phone_number:)
#client.messages.create(
from: ENV['TWILIO_PHONE_NUMBER'],
to: "+370#{phone_number}",
body: "Your pin is #{##pin}"
)
end
def self.verify(entered_pin)
puts "#{##pin}, pin #{entered_pin} entered"
if ##pin == entered_pin
Current.user.profile.update(verified: true)
else
return
end
end
And Profiles#verify :
def verify
SmsTool.verify(params[:pin])
#profile = current_user.profile
respond_to do |format|
format.js
end
if #profile.verified
redirect_to root_path, notice: 'Account created'
end
end
So what I dont like is SmsTool - as you see I use class variable - couldn't find another way. Also I created a separate Current module just to access Devise current_user object.. :
module Current
thread_mattr_accessor :user
end
ApplicationController:
around_action :set_current_user
def set_current_user
Current.user = current_user
yield
ensure
# to address the thread variable leak issues in Puma/Thin webserver
Current.user = nil
end
And as I mentioned above - I can't find a way how to implement a feature that sends pin code again (if user didn't received it).
And please - feel free to suggest elegant implementations.
p.s. this is my longest post yet. Sorry for that, but I think all info was needed to show you.
UPDATE:
So to resend pin was easy, I just added:
<div id="hiddenUnlessWrongPin">
<%= button_to "Re-send pin", action: "send_pin_again" %>
</div>
and action:
def send_pin_again
#phone_number = current_user.profile.phone
SmsTool.generate_pin
SmsTool.send_pin(phone_number: #phone_number)
end
But I still don't know how to stop sending pin if user already sent three of them. Only way I see is to make new row in db with integer value and increment it every time user sends pin. Is it the only way?
A good starting point would be to look at the Devise::Confirmable module which handles email confirmation. What I really like about it is that it models confirmations as a plain old resource.
I would try something similar but with a seperate model as it makes it really easy to add a time based limit.
class User < ApplicationRecord
has_one :profile
has_many :activations, through: :profiles
end
class Profile < ApplicationRecord
belongs_to :user
has_many :activations
end
# columns:
# - pin [int or string]
# - profile_id [int] - foreign_key
# - confirmed_at [datetime]
class Activation < ApplicationRecord
belongs_to :profile
has_one :user, through: :profile
delegate :phone_number, to: :profile
authenticate :resend_limit, if: :new_record?
authenticate :valid_pin, unless: :new_record?
attr_accessor :response_pin
after_initialize :set_random_pin!, if: :new_record?
def set_random_pin!
self.pin = rand(0000..9999).to_s.rjust(4, "0")
end
def resend_limit
if self.profile.activations.where(created_at: (1.day.ago..Time.now)).count >= 3
errors.add(:base, 'You have reached the maximum allow number of reminders!')
end
end
def valid_pin
unless response_pin.present? && response_pin == pin
errors.add(:response_pin, 'Incorrect pin number')
end
end
def send_sms!
// #todo add logic to send sms
end
end
Feel free to come up with a better name. Additionally this allows you to use plain old rails validations to handle the logic.
You can then CRUD it like any other resource:
devise_scope :user do
resources :activations, only: [:new, :create, :edit, :update]
end
class ActivationsController < ApplicationController
before_action :authenticate_user!
before_action :set_profile
before_action :set_activation, only: [:edit, :update]
# Form to resend a pin notification.
# GET /users/activations/new
def new
#activation = #profile.phone_authentication.new
end
# POST /users/activations/new
def create
#activation = #profile.phone_authentication.new
if #activation.save
#activation.send_sms!
redirect_to edit_user_phone_activations_path(#activation)
else
render :new
end
end
# Renders form where user enters the activation code
# GET /users/activations/:id/edit
def edit
end
# confirms the users entered the correct pin number.
# PATCH /users/activations/:id
def update
if #activation.update(update_params)
# cleans up
#profile.activations.where.not(id: #activation.id).destroy_all
redirect_to profile_path(#profile), success: 'Your account was activated'
else
render :edit
end
end
private
def update_params
params.require(:activation)
.permit(:response_pin)
.merge(confirmed_at: Time.now)
end
def set_profile
#profile = current_user.profile
end
def set_activation
#profile.activations.find(params[:id])
end
end
app/views/activations/new.html.erb:
<%= form_for(#activation) do |f| %>
<%= f.submit("Send activation to #{#activation.phone_number}") %>
<% end %>
No activation SMS? <%= link_ to "Resend", new_user_activation_path %>
app/views/activations/edit.html.erb:
<%= form_for(#activation) do |f| %>
<%= f.text_field :response_pin %>
<%= f.submit("Confirm") %>
<% end %>
I have the following set up in rails:
Document has_many Sections
Section belongs_to Document
The Section form is completed in the documents/show view...the Document controller for this action is:
def show
#document = current_user.documents.find(params[:id])
#section = Section.new if logged_in?
end
The Section form in documents/show is as follows:
<%= form_for(#section) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class="field">
<%= f.text_area :content, placeholder: "Compose new section..." %>
</div>
<%= hidden_field_tag :document_id, #document.id %>
<%= f.submit "Post", class: "btn btn-primary" %>
<% end %>
Where you can see a hidden_field_tag is sending the document_id
The sections_controller is as follows:
class SectionsController < ApplicationController
before_action :logged_in_user, only: [:create, :destroy, :show, :index]
def create
#document = Document.find(params[:document_id])
#section = #document.build(section_params)
if #section.save
flash[:success] = "Section created!"
redirect_to user_path(current_user)
else
render 'static_pages/home'
end
end
def destroy
end
def index
end
private
def section_params
params.require(:section).permit(:content)
end
end
I get the following error which I have not been able to resolve.
**NoMethodError (undefined method `build' for #<Document:0x00000004e48640>):
app/controllers/sections_controller.rb:6:in `create'**
I am sure it must be something simple I am overlooking but can't seem to find it. Any help would be appreciated:
Replace the below line :-
#section = #document.build(section_params)
with
#section = #document.sections.build(section_params)
You have a has_many associations named sections in the Document model. Thus as per the guide, you got the method collection.build(attributes = {}, ...). Read the section 4.3.1.14 collection.build(attributes = {}, ...) under the link I gave to you.
I posted an earlier question about this and was advised to read lots of relevant info. I have read it and tried implementing about 30 different solutions. None of which have worked for me.
Here's what I've got.
I have a Miniatures model.
I have a Manufacturers model.
Miniatures have many manufacturers THROUGH a Productions model.
The associations seem to be set up correctly as I can show them in my views and create them via the console. Where I have a problem is in letting the Miniatures NEW and EDIT views create and update to the Productions table.
In the console the command #miniature.productions.create(manufacturer_id: 1) works, which leads me to believe I should be able to do the same in a form.
I THINK my problem is always in the Miniatures Controller and specifically the CREATE function. I have tried out a ton of other peoples solutions there and none have done the trick. It is also possible that my field_for stuff in my form is wrong but that seems less fiddly.
I've been stuck on this for days and while there are other things I could work on, if this association isn't possible then I'd need to rethink my entire application.
The form now creates a line in the Productions table but doesn't include the all important manufacturer_id.
Any help VERY much appreciated.
My New Miniature form
<% provide(:title, 'Add miniature') %>
<h1>Add a miniature</h1>
<div class="row">
<div class="span6 offset3">
<%= form_for(#miniature) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.fields_for :production do |production_fields| %>
<%= production_fields.label :manufacturer_id, "Manufacturer" %>
<%= production_fields.select :manufacturer_id, options_from_collection_for_select(Manufacturer.all, :id, :name) %>
<% end %>
<%= f.label :release_date %>
<%= f.date_select :release_date, :start_year => Date.current.year, :end_year => 1970, :include_blank => true %>
<%= f.submit "Add miniature", class: "btn btn-large btn-primary" %>
<% end %>
</div>
</div>
Miniatures controller
class MiniaturesController < ApplicationController
before_action :signed_in_user, only: [:new, :create, :edit, :update]
before_action :admin_user, only: :destroy
def productions
#production = #miniature.productions
end
def show
#miniature = Miniature.find(params[:id])
end
def new
#miniature = Miniature.new
end
def edit
#miniature = Miniature.find(params[:id])
end
def update
#miniature = Miniature.find(params[:id])
if #miniature.update_attributes(miniature_params)
flash[:success] = "Miniature updated"
redirect_to #miniature
else
render 'edit'
end
end
def index
#miniatures = Miniature.paginate(page: params[:page])
end
def create
#miniature = Miniature.new(miniature_params)
if #miniature.save
#production = #miniature.productions.create
redirect_to #miniature
else
render 'new'
end
end
def destroy
Miniature.find(params[:id]).destroy
flash[:success] = "Miniature destroyed."
redirect_to miniatures_url
end
private
def miniature_params
params.require(:miniature).permit(:name, :release_date, :material, :scale, :production, :production_attributes)
end
def admin_user
redirect_to(root_url) unless current_user.admin?
end
def signed_in_user
unless signed_in?
store_location
redirect_to signin_url, notice: "Please sign in."
end
end
end
Miniature model
class Miniature < ActiveRecord::Base
has_many :productions, dependent: :destroy
has_many :manufacturers, :through => :productions
accepts_nested_attributes_for :productions
validates :name, presence: true, length: { maximum: 50 }
validates :material, presence: true
validates :scale, presence: true
validates_date :release_date, :allow_blank => true
def name=(s)
super s.titleize
end
end
Production model
class Production < ActiveRecord::Base
belongs_to :miniature
belongs_to :manufacturer
end
Manufacturer model
class Manufacturer < ActiveRecord::Base
has_many :productions
has_many :miniatures, :through => :productions
validates :name, presence: true, length: { maximum: 50 }
accepts_nested_attributes_for :productions
end
Instead of calling:
#production = #miniature.productions.create
Try Rails' "build" method:
def new
#miniature = Miniature.new(miniature_params)
#miniature.productions.build
end
def create
#miniature = Miniature.new(miniature_params)
if #miniature.save
redirect_to #miniature
else
render 'new'
end
end
Using the build method uses ActiveRecord's Autosave Association functionality.
See http://api.rubyonrails.org/classes/ActiveRecord/AutosaveAssociation.html
You also need to update your params method, e.g.
def miniature_params
params.require(:miniature).permit(:name, :release_date, :material, :scale, productions_attributes: [:manufacturer_id])
end
Also your fields_for should be plural (I think)...
<%= f.fields_for :productions do |production_fields| %>
So I have a Review model in my app for users leaving reviews for movies. I'm rendering a form for a new Review in the Show view for each individual movie. However, when I try to submit a blank review to check that the errors display on the page, they don't. What could I be doing wrong?
My review model:
class Review < ActiveRecord::Base
attr_accessible :content
belongs_to :user
belongs_to :movie
validates :user_id, presence: true
validates :movie_id, presence: true
validates :content, presence: true, length: { maximum: 1000 }
end
My reviews controller:
class ReviewsController < ApplicationController
before_filter :signed_in_user, only: [:new, :create, :destroy]
def new
end
def create
#I'm doing this to get around the mass assignment error.
movie_id = params[:review].delete(:movie_id)
#review = current_user.reviews.build(params[:review])
#review.movie_id = movie_id
if #review.save
flash[:success] = "Review created!"
redirect_to movie_path(#review.movie)
else
redirect_to movie_path(#review.movie)
end
end
def destroy
end
end
My form:
<%= form_for(:review, url: reviews_path) do |f| %>
<%= f.hidden_field :movie_id %>
<div class="field">
<%= f.text_area :content, placeholder: "Write a new review..." %>
</div>
<%= f.submit "Submit", class: "btn btn-large btn-primary" %>
<% end %>
When you redirect away from the #create method, you're losing the errors... Change to
if #review.save
flash[:success] = "Review created!"
redirect_to movie_path(#review.movie)
else
render :new
end