I have this app where a user can write a review for a school. A user must sign in with Facebook to save a review. The problem is if a user is unsigned and writes a review, then signs in with Facebook they have to write the same review again.
I am trying to fix this by storing the review data form in sessions, but I cant quite make it work.
What is the proper rails way to do this?
ReviewForm:
<%= form_for [#school, Review.new] do |f| %>
<%= f.text_area :content %>
<% if current_user %>
<%= f.submit 'Save my review', :class => "btn" %>
<% else %>
<%= f.submit 'Save my review and sign me into facebook', :class => "btn" %>
<% end %>
<%end %>
ReviewController
class ReviewsController < ApplicationController
before_filter :signed_in_user, only: [:create, :destroy]
def create
#school = School.find(params[:school_id])
#review = #school.reviews.new(params[:review])
#review.user_id = current_user.id
if #review.save
redirect_to #review.school, notice: "Review has been created."
else
render :new
end
end
def new
#school = School.find_by_id(params[:school_id])
#review = Review.new
end
def save_review(school, review, rating)
Review.create(:content => review, :school_id => school,
:user_id => current_user, :rating => rating)
end
private
def signed_in?
!current_user.nil?
end
def signed_in_user
unless signed_in?
# Save review data into sessions
session[:school] = School.find(params[:school_id])
session[:review] = params[:review]
session[:rating] = params[:rating]
# Login the user to facebook
redirect_to "/auth/facebook"
# After login save review data for user
save_review(session[:school], session[:review], session[:rating])
end
end
end
My understanding is that it's not "The Rails Way" to store things in the session besides really tiny stuff like a user token, etc. You can read more about that idea in The Rails 3 Way by Obie Fernandez.
I would recommend that you store reviews in the database right from the start and only "surface" the review after the review has been connected to a Facebook-authenticated user. If you have any curiosities regarding how to accomplish that, I'm happy to elaborate.
Edit: here's a little sample code. First I'd take care of associating users with reviews, for "permanent" storage. You could just add a user_id to the review table, but it would probably be null most of the time, and that seems sloppy to me:
$ rails g model UserReview review_id:references, user_id:references
Then I'd create a user_session_review table with a review_id and a user_session_token. This is for "temporary" storage:
$ rails g model UserSessionReview review_id:integer, user_session_token:string
Then when a user signs up, associate any "temporary" reviews with that user:
class User
has_many :user_reviews
has_many :reviews, through: :user_reviews
has_many :user_session_reviews
def associate_reviews_from_token(user_session_token)
temp_reviews = UserSessionReview.find_all_by_user_session_token(user_session_token)
temp_reviews.each do |temp_review|
user_reviews.create!(review_id: temp_review.review_id)
temp_review.destroy
end
end
end
So in your controller, you might do
class UsersController < ApplicationController
def create
# some stuff
#user.associate_reviews_from_token(cookies[:user_session_token])
end
end
You'll of course have to read between the lines a little bit, but I think that should get you going.
Edit 2: To delete old abandoned reviews, I'd do something like this:
class UserSessionReview
scope :old, -> { where('created_at < ?', Time.zone.now - 1.month) }
end
Then, in a cron job:
UserSessionReview.old.destroy_all
You should save the review in the create sessions action (which is not included in your question). Assuming you are using omniauth, you can add something on the action that handles the callback
# review controller
def signed_in_user
unless signed_in?
# Save review data into sessions
session[:school] = School.find(params[:school_id])
session[:review] = params[:review]
session[:rating] = params[:rating]
# Login the user to facebook
redirect_to "/auth/facebook"
end
end
# callback to login the user
def handle_callback
# do your thing here to login the user
# once you have the user logged in
if signed_in?
if session[:school] && session[:review] && session[:rating] # or just 1 check
Review.create(
content: session.delete(:review),
school_id: session.delete(:school),
user_id: current_user.id,
rating: session.delete(:rating)
)
#redirect_to somewhere
end
end
end
I used delete so the session will be cleared of these values.
UPDATE: since you're using a session controller
class SessionsController < ApplicationController
def create
if user = User.from_omniauth(env["omniauth.auth"])
session[:user_id] = user.id
if session[:school] && session[:review] && session[:rating] # or just 1 check
review = Review.new
review.content = session.delete(:review)
review.school_id = session.delete(:school)
review.user_id = user.id
review.rating = session.delete(:rating)
review.save
end
end
redirect_to :back
end
Related
So I am in the process of setting up a forum and everything is setup/working well except for my replies are not appearing on the thread "show" page. After checking the rails console, I see they are saving but the user_id and discussion_id are no. The user_id is always nil and the discussion_id is always 0. The discussion threads were easier to setup but with having these replies, I obviously seem to be having an issue. Here are my snippets of code:
class PostsController
# ...
before_filter :authenticate_user!
before_filter :set_discussion, only: [:new, :create, :destroy]
def create
#post = #discussion.post.new(create_params) do |post|
post.user = current_user
end
if #post.save
redirect_to #discussion, notice: "It has been posted!"
else
render :new
end
end
def destroy
#post = #discussion.posts.find(params[:id])
#post.destroy
flash.notice = "Deleted"
redirect_to discussion_path(#discussion)
end
private
def create_params
params.require(:post).permit(:reply)
end
def set_discussion
#discussion = Discussion.friendly.find(params[:id])
end
end
class DiscussionsController
def show
#discussion = Discussion.friendly.find(params[:id])
#post = Post.new
render :layout => 'discussion'
end
end
Partial rendered to reply:
<h2>Reply</h2>
<%= form_for [ #discussion, #post ] do |f| %>
<p>
<%= f.label :reply, "Reply" %><br/>
<%= f.text_field :reply %>
</p>
<p>
<%= f.submit 'Submit' %>
</p>
<% end %>
Partial rendered to show replies in on discussion page:
<h3><%= post.user.first_name %></h3>
<%= post.reply %>
Posted: <%= post.created_at.strftime("%b. %d %Y") %></p>
<p><%= link_to "Delete Comment", [post.discussion, post], data: {confirm: "Are you sure you wish to delete?"}, method: :delete, :class => "post_choices" %></p>
Just want to mention that I also have the correct associations between the three models (User, Discussion, Post). If there is more code needed, please let me know. I appreciate it very much for any information that may be helpful =)
Joe
EDIT
class User < ActiveRecord::Base
has_many :articles
has_many :discussions
has_many :posts
# ...
end
class Discussion
belongs_to :user
has_many :posts
extend FriendlyId
friendly_id :subject, use: :slugged
end
class Post
belongs_to :discussion
belongs_to :user
end
I could post the entire user model if needed but its all validations/devise aspects =P The other two I listed all of the contents in the models.
Edit 2
Thanks to Max, the user_id returns correctly in the console but still not the discussions. Going go dig around a bit more with the recent changes to see what else =)
There are a few issue you need to deal with.
First you should ensure that Devise is actually authorizing your controller action.
class PostsController < ApplicationController
before_filter :authenticate_user!
end
Otherwise current_user will return nil if there is no signed in user. And I'm
guessing that you do not want un-authenticated users to be able to create posts.
Also if you have a nested route you most likely want to check that the discussion actually
exists before trying to add posts.
class PostsController
before_filter :authenticate_user!
before_filter :set_discussion, only: [:new, :create, :destroy]
private
# Will raise an ActiveRecord::NotFoundError
# if the Discussion does not exist
def set_discussion
#discussion = Discussion.friendly.find(params[:id])
end
end
When you are creating resources be careful not to query the database needlessly.
This especially applies to CREATE and UPDATE queries which are expensive.
def create
#post = Post.create(post_params) # INSERT INTO 'users'
#post.discussion_id = params[:discussion_id]
#post.user = current_user
#post.save # UPDATE 'users'
flash.notice = "It has been posted!"
redirect_to discussions_path(#post.discussion)
end
Also you are not even checking if the record was created successfully.
So lets put it all together:
class PostsController
before_filter :authenticate_user!
before_filter :set_discussion, only: [:new, :create, :destroy]
def new
#post = #discussion.post.new
end
def create
# new does not insert the record into the database
#post = #discussion.post.new(create_params) do |post|
post.user = current_user
end
if #post.save
redirect_to #discussion, notice: "It has been posted!"
else
render :new # or redirect back
end
end
def destroy
#post = #discussion.posts.find(params[:id])
#post.destroy
flash.notice = "Deleted"
redirect_to discussion_path(#discussion)
end
private
def create_params
# Only permit the params which the user should actually send!
params.require(:post).permit(:reply)
end
# Will raise an ActiveRecord::NotFoundError
# if the Discussion does not exist
def set_discussion
#discussion = Discussion.friendly.find(params[:id])
end
end
I am using Rails 4.
I have subarticles nested into articles. I am storing all of the form data from subarticles in a session when a user needs to create an account before submission.
Here is what I am using (subarticles_controller):
def create
if current_user.nil?
session[:subarticle] = params
redirect_to new_user_session_path
end
Then after the user signs up, it creates the subarticle with the stored params using
if session[:subarticle].present?
#subarticle = current_user.subarticles.create(session[:subarticle]["subarticle"])
session[:subarticle] = nil
flash[:notice] = "Awesome, you are logged in and your answer is undergoing review."
edit_user_registration_path
end
I am having trouble, however, saving the article_id in which the subarticle is created under. Can someone point me in the right direction to doing this?
A better approach could be to save the (sub)articles created by guest users in the database.
class SubArticlesController < ApplicationController
before_action :set_article, only: [:show, :edit, :update, :destroy]
# ...
def create
#subarticle = Article.new(article_params) do |a|
if current_user
a.user = current_user
else
a.token = SecureRandom.hex
end
end
if #subarticle.save
if #subarticle.user
redirect_to #subarticle
else
session[:after_sign_in_path] = edit_article_path(#subarticle, token: #subarticle.token)
redirect_to new_user_session_path, notice: 'Please sign in to finalize your article.'
end
else
render :new
end
end
def edit
if #subarticle.user.nil? && #subarticle.token != params[:token]
redirect_to root_path, alert: 'You are not authorized.'
end
flash[:notice] = 'Please press save again to publish your post.' unless #subarticle.user
render :edit
end
def update
# The #subarticle.token should be included in the edit form
unless #subarticle.user && #subarticle.token == params[:sub_article][:token]
# let the current user claim the article
#subarticle.user = current_user
end
if #subarticle.update(article_params)
redirect_to #subarticle
else
render :edit
end
end
private
def set_article
#subarticle = Article.find(params[:id])
end
def sub_article_params
params.require(:sub_article).permit(...)
end
end
So here we we instead give the user a link to the edit page for the article where he/she can "finish" the article after logging in.
Since a malicious user could potentially "steal" unclaimed articles by guessing the id and entering the edit url we add a random token which we store in the database with the article and add to the url. Not 100% foolproof but at least better.
To make this work you will also need to add a token field to your form:
<%= form_for(#subarticle) do |f| %>
...
<%= f.hidden_field :token %>
...
<% end %>
The reason you might want to consider this is because session storage is often memory based and if you have a large amount of traffic storing the entire params hash in the session will exhaust the server memory. Also you should reset the session before logging a user in or out to avoid session fixation.
We do have a few issues though - first we don't want to accumulate a bunch of "unclaimed" articles if the user (or a bot) never logs in. The easiest way to do this is to setup a cron job to delete articles over a certain age without an associated user.
You would also want to filter any articles without a user from you show/index action.
So I am making a site where users can only submit a post once, and then the "new post" button goes away forever.
I would also like to put a limit on the overall amount of posts. So, only the first 100 or so people can actually post.
I used rails generate scaffold to build the posting system.
I don't know where to start.
Thanks!
You can either create a constant if all user will have the same limit, or add a field in your user record if you plan for each user to have different limits.
Then you create a validator which check the number of existing posts and forbid creation of new posts if the limit is reached
More info in rails guide: http://guides.rubyonrails.org/active_record_validations.html#performing-custom-validations
An alternative approach is using a policy object. Here's how I would approach this using Pundit.
Updated:
app/models/post.rb
class Post < ActiveRecord::Base
belongs_to :user
def self.limit_exceeded?(max = 100)
count >= max
end
end
app/models/user.rb
class User < ActiveRecord::Base
has_one :post
end
app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def create?
!user_has_post? && space_to_post?
end
private
def user_has_post?
user.post.present?
end
def space_to_post?
!Post.limit_exceeded?
end
end
app/controllers/posts_controller.rb
class PostsController < ApplicationController
def show
#post = Post.find(params[:id])
end
def new
#post = Post.new
end
def create
authorize(:post)
#post = current_user.build_post(post_params)
if #post.save
redirect_to #post, notice: "Your post was created!"
else
render :new
end
end
private
def post_params
params.require(:post).permit(:message)
end
end
app/view/posts/new.html.erb
<% if policy(:post).create? %>
<%= form_for(#post) do |form| %>
<%= form.text_area :message %>
<%= form.submit "Post" %>
<% end %>
<% else %>
You cannot post.
<% end %>
This code assumes the user is authenticated. If you haven't incorporated authentication, you'll need to use a gem for that, or roll your own implementation. I'd recommend Devise or Clearance.
Good luck!
Trying to update 2 attributes to a User model, this is my current code in the Users controller:
def update
#user = User.find(params[:id])
if #user.update_attributes(songkickID: params[:user][:songkickID], jamID: params[:user][:jamID])
redirect_to #user
else
redirect_to #user
end
end
The Songkick ID and the Jam ID are entered into 2 different fields. However, with the current code, if I attempt to update the Jam ID on its own, it updates that attribute, but then redirects to the user page (as expected), where the Songkick ID is now nil. Upon entering the Songkick ID again, the Jam ID becomes nil. I suppose this is because they are both part of the same if statement in the controller?
I attempted to use an elsif for the jamID params, but it does not seem to recognise at all (i.e. won't update that attribute for the user). Also attempted || conditional operator.
EDIT: Here's the 2 different forms:
<%= form_for(#user) do |f| %>
<%= f.text_field :jamID, :id=>"jamURL" %>
<%= f.submit "Jam ID", :onclick => "changeImg()", id: "saveJam" %>
<% end %>
and
<%= form_for(#user) do |f| %>
<%= f.text_field :songkickID %>
<%= f.submit "Songkick ID", :type => :image, :src => image_path("songkicklogo.png"), id: "skLogo" %>
<% end %>
And I tried modifiying the code to update_column, but I get wrong number of arguments (1 for 2).
EDIT 2: Following layout from Hartl's Rails Tutorial, I attempted this to define strong parameters:
private
def user_params
params.require(:user).permit(:songkickID, :jamID)
end
But I still get the Forbidden Attributes Error?
EDIT 3: The following code passes, but I worry it doesn't adhere to Rails 4 strong parameters:
Controller:
class UsersController < ApplicationController
def show
#user = User.find(params[:id])
end
def edit
#user = User.find(params[:id])
end
def user_params
params.require(:user).permit(:songkickID, :jamID)
end
def update
#user = User.find(params[:id])
if #user.update_attributes(user_params)
redirect_to #user
else
redirect_to #user
end
end
end
If I move update to below the update method, I get an undefined variable/method error for user_params, and I cannot make it private.
So - why are you explicitly naming the attributes in your update_attributes?
You should be able to use the following:
#user.update_attributes(params[:user])
Remember that if you've named your form fields correctly, params[:user] is a hash that will already have the keys you want (:songkickID etc)
Now - you will get one of two things coming through to your action, which you then pass through to update_attributes as:
{:songkickID => someID}
{:jamID => someOtherID}
which will correctly update your user and only change the one that is passed.
The problem with your earlier code was that what you passed to update attribute was:
{:songkickID => someID, :jamID => nil}
{:songkickID => nil, :jamID => someOtherID}
which was deliberately overwriting the other id with the nil you passed.
EDIT from OP: Thanks for this, and here's my final controller code:
class UsersController < ApplicationController
def show
#user = User.find(params[:id])
end
def edit
#user = User.find(params[:id])
end
def update
#user = User.find(params[:id])
if #user.update_attributes(user_params)
redirect_to #user
else
redirect_to #user
end
end
private
def user_params
params.require(:user).permit(:songkickID, :jamID)
end
end
In last case scenario:
def update
if params[:user][:songkickID]
received_param = { songkickID: params[:user][:songkickID] }
elsif params[:user][:jamID]
received_param = { jamID: params[:user][:jamID] }
end
#user.update_attributes(received_param)
redirect_to #user
end
Note: I removed the last condition since it wasn't useful
I followed Railscast #393 to implement guest users into my application. The only problem I'm having with this approach is that it requires the user to still click a button to create the user (albeit without signing up). My goal is to have this occur automatically, but without happening every time the page is reloaded or visited again. I'm at a loss on how to go about this.
user_controller.rb
class UsersController < ApplicationController
def new
#user = User.new
end
def create
#user = params[:user] ? User.new(params[:user]) : User.new_guest
if #user.save
current_user.move_to(#user) if current_user && current_user.guest?
session[:user_id] = #user.id
redirect_to root_url
else
render "new"
end
end
end
user.rb
class User < ActiveRecord::Base
attr_accessible :name, :provider, :uid, :email
has_many :posts, :dependent => :destroy
has_many :comments, :dependent => :destroy
def self.new_guest
new { |u| u.guest = true }
end
def move_to(user)
posts.update_all(user_id: user.id)
comments.update_all(user_id: user.id)
end
end
application.html.erb
<ul>
<li><%= button_to "Try it for free!", users_path, method: :post %></li>
</ul>
I can provide any additional information necessary to help answer this question.
You could potentially handle this in a before_filter, but you'd want to consider that you'd be creating a new guest user any time that anyone or anything (Google bot, for instance) requests a page from your site without an existing session. Requiring some sort of user action to kick it off is probably a good thing. That being said, if you really wanted to do it you could do something like this in your ApplicationController:
before_filter :create_guest_if_needed
def create_guest_if_needed
return if session[:user_id] # already logged in, don't need to create another one
#user = User.new_guest
#user.save
session[:user_id] = #user.id
# do anything else you need here...
end