Setting database values conditionally in the model - ruby-on-rails

Essentially:
I want to set the title of a revision to the title of the post it is a revision of.
I want to set the revision_id to the id if the new post is not a revision (or is_revision = false)
So far, I'm having a lot of trouble with this. Any help appreciated.
post.rb
class Post < ActiveRecord::Base
has_many :revisions, class_name: "Post", foreign_key: "revision_id"
belongs_to :revision, class_name: "Post"
#...
before_save :post_title
def post_title
if :is_revision == false
#revision_id = #post_id
elsif :is_revision == true
:revision_id != :post_id
#title = #revision.title
end
end
#...
end
Update
I am now able to get this working through the controller. Should I still aim to make this work occur in the model? The same code is under create and update actions.
posts_controller.rb
def create
#post = current_user.posts.new(params[:post])
if #post.save
if #post.is_revision == false
#post.revision_id = #post.id
#post.save
elsif #post.is_revision == true
#post.title = #post.revision.title
#post.save
end
end
end

I don't quite understand your logic there, maybe you should tell us some context.
Otherwise, did you simply try:
class Post < ActiveRecord::Base
has_many :revisions, class_name: "Post", foreign_key: "revision_id"
belongs_to :revision, class_name: "Post"
#...
before_save :set_title_to_revision_title, :if => :is_revision?
# Need to be called after the save because post.id will not be available on the create
after_save :update_revision_id_to_id, :unless => :is_revision?
def is_revision?
#is_revision
end
def set_title_to_revision_title
#title = #revision.title
end
def update_revision_id_to_id
# just make a quick call to update it i
update_attribute :revision_id, #id
end
#...
end

Related

DB rolls back on create action

I'm trying to create a form with a series of checks to prevent duplicates during the simultaneous creation of three model records: one for the parent (assuming it doesn't exist), one for its child (assuming it doesn't exist), and one for a join table between the child and the User (to allow the User to have their own copy of the Song object).
In the current state of the code, The checks seemingly pass, but
the server logs show ROLLBACK, and nothing gets saved
to the database EXCEPT the parent object (artist).
When I try to use the ids of the object, I get the error undefined method id for nil:NilClass, or "couldn't find object without an ID".
The following code is in my controller:
class SongsController < ApplicationController
before_action :authenticate_user!
def create
#artist = Artist.find_by(name: params[:artist][:name].strip.titleize) #look for the artist
#song = Song.find_by(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize)
if #artist.present? && #song.present?
#user_song = current_user.user_songs.find(#song_id)
if #user_song.present?
render html: "THIS SONG IS ALREADY IN YOUR PLAYLIST"
render action: :new
else
#user_song = UserSong.create(user_id: current_user.id, song_id: #song.id)
redirect_to root_path
end
elsif #artist.present? && !#song.present?
#song = #artist.songs.build(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize, lyrics: params[:artist][:songs_attributes]["0"][:lyrics].strip)
#user_song = UserSong.create(user_id: current_user.id, song_id: #song.id)
redirect_to root_path
elsif !#artist.present?
#artist = Artist.create(name: params[:artist][:name].strip.titleize)
#song = #artist.songs.build(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize, lyrics: params[:artist][:songs_attributes]["0"][:lyrics].strip)
#user_song = UserSong.create(user_id: current_user.id, song_id: #song.id)
redirect_to root_path
else
render html: "SOMETHING WENT WRONG. CONTACT ME TO LET ME KNOW IF YOU SEE THIS MESSAGE"
end
end
def index
#songs = Song.all
end
def new
#artist = Artist.new
#artist.songs.build
#user_song = UserSong.new(user_id: current_user.id, song_id: #song_id)
end
def show
#song_id = params["song_id"]
#song = Song.find(params[:id])
end
def destroy
UserSong.where(:song_id => params[:id]).first.destroy
flash[:success] = "The song has been from your playlist"
redirect_to root_path
end
def edit
#song = Song.find(params[:id])
#artist = Artist.find(#song.artist_id)
end
def update
end
private
def set_artist
#artist = Artist.find(params[:id])
end
def artist_params
params.require(:artist).permit(:name, songs_attributes: [:id, :title, :lyrics])
end
def set_song
#song = Song.find(params["song_id"])
end
end
The models:
class Artist < ApplicationRecord
has_many :songs
accepts_nested_attributes_for :songs, reject_if: proc { |attributes| attributes['lyrics'].blank? }
end
class Song < ApplicationRecord
belongs_to :artist
has_many :user_songs
has_many :users, :through => :user_songs
end
class UserSong < ApplicationRecord
belongs_to :song
belongs_to :user
end
Sorry if I haven't abstracted enough. Not really sure how, given that there's no error message, just a rollback (without any validations present in any of the controllers).
Thanks to #coreyward and his pointing out of the fat-model skinny-controller lemma (never knew that was a thing), I was able to cut the code down and arrive at a solution immediately. In my models, I used validates_uniqueness_of and scope in order to prevent duplication of records. In my controller, I used find_or_create_by to seal the deal.
To whom it may concern, the final code is as follows:
class SongsController < ApplicationController
before_action :authenticate_user!
def create
#artist = Artist.find_or_create_by(name: params[:artist][:name].strip.titleize)
#song = #artist.songs.find_or_create_by(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize) do |song|
song.lyrics = params[:artist][:songs_attributes]["0"][:lyrics].strip
end
#user_song = current_user.user_songs.find_or_create_by(song_id: #song.id) do |user_id|
user_id.user_id = current_user.id
end
redirect_to root_path
end
class Song < ApplicationRecord
validates_uniqueness_of :title, scope: :artist_id
belongs_to :artist
has_many :user_songs
has_many :users, :through => :user_songs
end
class Artist < ApplicationRecord
validates_uniqueness_of :name
has_many :songs
accepts_nested_attributes_for :songs, reject_if: proc { |attributes| attributes['lyrics'].blank? }
end
class UserSong < ApplicationRecord
validates_uniqueness_of :song_id, scope: :user_id
belongs_to :song
belongs_to :user
end

RoR: Save information from 3 models at same time

I am trying to make it so that when I save an answer, I also save the prop_id that is associated with that answer.
I have a nested route relationship so that each prop (stands for proposition or bet) has a an associated answer like this: http://localhost:3000/props/1/answers/new.
Right now, when I save an answer, I save the answer choice and the user_id who created the answer. I need to save also the prop that is associated with the answer.
Answers Controller:
class AnswersController < ApplicationController
attr_accessor :user, :answer
def index
end
def new
#prop = Prop.find(params[:prop_id])
#user = User.find(session[:user_id])
#answer = Answer.new
end
def create
#prop = Prop.find(params[:prop_id])
#user = User.find(session[:user_id])
#answer = #user.answers.create(answer_params)
if #answer.save
redirect_to root_path
else
render 'new'
end
end
def show
#answer = Answer.find params[:id]
end
end
private
def answer_params
params.require(:answer).permit(:choice, :id, :prop_id)
end
Answer Model
class Answer < ActiveRecord::Base
belongs_to :prop
belongs_to :created_by, :class_name => "User", :foreign_key => "created_by"
has_many :users
end
Prop Model
class Prop < ActiveRecord::Base
belongs_to :user
has_many :comments
has_many :answers
end
User Model
class User < ActiveRecord::Base
has_many :props
has_many :answers
has_many :created_answers, :class_name => "Answer", :foreign_key => "created_by"
before_save { self.email = email.downcase }
validates :username, presence: true, uniqueness: {case_sensitive: false}, length: {minimum: 3, maximum: 25}
has_secure_password
end
Just modify your code a little bit, and it will work:
def create
#user = User.find(session[:user_id])
#prop = #user.props.find_by(id: params[:prop_id])
#answer = #user.answers.build(answer_params)
#answer.prop = #prop
# Modify #user, #prop or #answer here
# This will save #user, #prop & #answer
if #user.save
redirect_to root_path
else
render 'new'
end
end

private messaging system in rails

I am working on a messaging system in my rails app. I already have it working properly for sending messages between 2 users(sender and recipient). This setup is fine but how can I make a new conversation for each room so the uniqueness checking will be only between an user and a room or viceversa?? Each user is only allowed to send message to a room from the room show page. So room_id can be fetched there. A single user can have many listings which makes it complicated for me.So am confused on what change to make in the below code to accomplish that??Or do I have to make a different design approach for the models?
I have a user, listing, conversation and message model
conversation.rb
class Conversation < ActiveRecord::Base
belongs_to :sender, foreign_key: :sender_id, class_name: 'User'
belongs_to :recipient, foreign_key: :recipient_id, class_name: 'User'
has_many :messages, dependent: :destroy
validates_uniqueness_of :sender_id, scope: :recipient_id
scope :involving, -> (user) do
where("conversations.sender_id = ? OR conversations.recipient_id = ?", user.id, user.id)
end
scope :between, -> (sender_id, recipient_id) do
where("(conversations.sender_id = ? AND conversations.recipient_id = ?) OR (conversations.sender_id = ? AND conversations.recipient_id = ?)",
sender_id, recipient_id, recipient_id, sender_id)
end
end
Message.rb
class Message < ActiveRecord::Base
belongs_to :conversation
belongs_to :user
validates_presence_of :content, :conversation_id, :user_id
def message_time
created_at.strftime("%v")
end
end
conversations_controller.rb
class ConversationsController < ApplicationController
before_action :authenticate_user!
def index
#conversations = Conversation.involving(current_user)
end
def create
if Conversation.between(params[:sender_id], params[:recipient_id]).present?
#conversation = Conversation.between(params[:sender_id], params[:recipient_id]).first
else
#conversation = Conversation.create(conversation_params)
end
redirect_to conversation_messages_path(#conversation)
end
private
def conversation_params
params.permit(:sender_id, :recipient_id)
end
end
messages_controller.rb
class MessagesController < ApplicationController
before_action :authenticate_user!
before_action :set_conversation
def index
if current_user == #conversation.sender || current_user == #conversation.recipient
#other = current_user == #conversation.sender ? #conversation.recipient : #conversation.sender
#messages = #conversation.messages.order("created_at DESC")
else
redirect_to conversations_path, alert: "You don't have permission to view this."
end
end
def create
#message = #conversation.messages.new(message_params)
#messages = #conversation.messages.order("created_at DESC")
if #message.save
redirect_to conversation_messages_path(#conversation)
end
end
private
def set_conversation
#conversation = Conversation.find(params[:conversation_id])
end
def message_params
params.require(:message).permit(:content, :user_id)
end
end
Your relations are off. A conversation where the sender and recipient are fixed is no good - in fact thats just a monolog!
Instead we need a real many to many relation. That means we need a third table to store the link between users and converstations
So lets start by generating a model:
rails g model UserConversation user:belongs_to conversation:belongs_to
This will generate a model and a migration for a join table which will link users and conversations. We should now also take care of the uniqueness requirement. Open up the migration:
class CreateUserConversations < ActiveRecord::Migration
def change
create_table :user_conversations do |t|
t.belongs_to :user, index: true, foreign_key: true
t.belongs_to :conversation, index: true, foreign_key: true
t.timestamps null: false
end
# Add this constraint
add_index :user_conversations, [:user_id, :conversation_id], unique: true
end
end
That constraint that ensures the uniqueness on the database level and protects against race conditions. We also want a validation on the software level.
class UserConversation < ActiveRecord::Base
belongs_to :user
belongs_to :conversation
validates_uniqueness_of :user_id, scope: :conversation_id
end
Now we setup the relations in User and Conversation so that they go through the join model:
class User < ActiveRecord::Base
has_many :user_conversations
has_many :conversations, through: user_conversations
def has_joined?(conversation)
conversations.where(id: conversation).exist?
end
end
class Conversation < ActiveRecord::Base
has_many :user_conversations
has_many :messages
has_many :users, through: user_conversations
def includes_user?(user)
users.where(id: user).exist?
end
end
This lets us do #user.conversations or #conversation.users. We don't need the hacky scopes.
This is an example of how you could possibly add a user to a conversation on the fly:
class MessagesController < ApplicationController
# ...
def create
unless current_user.has_joined?(conversation)
# #todo handle case where this fails
#conversation.users << current_user
end
#message = #conversation.messages.new(message_params) do |m|
# get the current user from the session or a token
# using params is an open invitation for hacking
m.user = current_user
end
if #message.save
redirect_to conversation_messages_path(#conversation)
else
render :new
end
end
# ...
end
But note that you still have quite a way to go and will likely need several different controllers to properly represent messages in different contexts:
/messages/:id => MessagesController
/users/:user_id/messages => Users::MessagesController
/conversations/:id/messages => Conversations::MessagesController

Passing in, retrieving, and setting restrictions of User from Post - Comment model in Rails

I'm attempting to set limits on the amount of commenting users can do on particular post during the day. I have implemented the following (successfully) in my Post model to limit the amount of Posts they can create.
class Post < ActiveRecord::Base
validate :daily_limit, :on => :create
def daily_limit
# Small limit for users who just sign up
if author.created_at >= 14.days.ago
if author.created_posts.today.count >= 4
errors.add(:base, "Exceeds Your Daily Trial Period Limit(4)")
end
else
if author.created_posts.today.count >= author.post_limit_day
errors.add(:base, "Exceeds Your Daily Limit")
end
end
end
end
But, when I attempt to add similar restrictions to my Comment model
class PostComment < ActiveRecord::Base
validate :daily_limit, :on => :create
belongs_to :post, :counter_cache => true
belongs_to :user
def daily_limit
# Small limit for users who just sign up
if user.posted_comments.today.count >= 2
errors.add(:base, "Exceeds Your Daily Trial Period Limit(4)")
end
end
end
I am greeted with a undefined method 'posted_comments' for nil:NilClass error. I don't believe my user_id is being passed into my model correctly in order to access it with something like user.posted_comments.today.count>=2
My create action in my post_comments controller is as follows:
class PostCommentsController < ApplicationController
def create
#post = Post.find(params[:post_id])
#post_comment = #post.post_comments.create(post_comment_params)
#post_comment.user = current_user
if #post_comment.save
redirect_to #post
else
flash[:alert] = "Comment Not Added"
redirect_to #post
end
end
end
and the my hacked down User model is as follows:
class User < ActiveRecord::Base
has_many :created_posts, class_name: 'Post', :foreign_key => "author_id",
dependent: :destroy
has_many :posted_comments, class_name: 'PostComment', :foreign_key =>"user_id", dependent: :destroy
end
Thanks.
You are assigning the user after "create" in your controller
#post_comment = #post.post_comments.create(post_comment_params)
#post_comment.user = current_user
Try this:
#post_comment = #post.post_comments.build(post_comment_params)
#post_comment.user = current_user

How to implement one vote per user per comment?

I currently have a comment controller that has the method vote_up and vote_down this is how my vote_up currently works.
My Comment model has description and a count field.
def vote_up
#comment = Comment.find(params[:comment_id])
#comment.count += 1
if #comment.save
flash[:notice] = "Thank you for voting"
respond_to do |format|
format.html { redirect_to show_question_path(#comment.question) }
format.js
end
else
flash[:notice] = "Error Voting Please Try Again"
redirect_to show_question_path(#comment.question)
end
end
This allows for multiple vote up and downs. How would I design it so that a user can only vote once per comment but somehow keep track if they voted up or down, so they have the ability to change their vote if they want too.
You could do something like this. It prohibits identical votes but allows changing the vote to the opposite (it's a thumbs up/thumbs down system).
def vote(value, user) # this goes to your model
#find vote for this instance by the given user OR create a new one
vote = votes.where(:user_id => user).first || votes.build(:user_id => user)
if value == :for
vote_value = 1
elsif value == :against
vote_value = -1
end
if vote.value != vote_value
vote.value = vote_value
vote.save
end
end
migration:
def self.up
create_table :votes do |t|
t.references :comment, :null => false
t.references :user, :null => false
t.integer :value, :null => false
end
add_index :votes, :post_id
add_index :votes, :user_id
add_index :votes, [:post_id, :user_id], :unique => true
end
Alternatively, you could use a gem called thumbs_up or any other.
class AnswersController < ApplicationsController
def vote
#params[:answer_id][:vote]
#it can be "1" or "-1"
#answer = Answer.find(params[:answer_id])
#answer.vote!(params[:answer_id][:vote])
end
def show
#answer = Answer.find(params[:answer_id])
#answer.votes.total_sum
end
end
class Answer < ActiveRecord::Base
has_many :votes do
def total_sum
votes.sum(:vote)
end
end
def vote!(t)
self.votes.create(:vote => t.to_i)
end
end
class Vote < ActiveRecord::Base
belongs_to :answer
belongs_to :user
validates_uniqueness_of :user_id, :scope => :answer_id
end
you could perhaps add a validation in your model to make sure that count is numerically equal to or less than 1
validates :count, :numericality => { :less_than_or_equal_to => 1 }

Resources