I have an app that has a blog feature. Originally when I set this up a post post consisted of a title, body, keywords, and image link. I want to be able to filter posts based on keywords and I think the cleanest way to do this is to move keyword to their own table and associate the two tables. So each posts can have multiple keywords and each keyword can have multiple posts.
I created a migrations and model for keywords.
class CreateKeywords < ActiveRecord::Migration[6.1]
def change
create_table :keywords do |t|
t.string :keyword
t.timestamps
end
end
end
class Keyword < ApplicationRecord
has_and_belongs_to_many :posts
end
I associated that with the posts table and changed the posts model.
class CreatePostsKeywordsJoinTable < ActiveRecord::Migration[6.1]
def change
create_join_table :posts, :keywords do |t|
t.index [:post_id, :keyword_id]
t.index [:keyword_id, :post_id]
end
end
end
class Post < ApplicationRecord
belongs_to :user
has_and_belongs_to_many :keywords
validates :title, presence: true
validates :body, presence: true
accepts_nested_attributes_for :keywords
# validates :keywords, presence: true
end
For now I just commented out the keywords in the Post model. I'm not exactly sure if I need to remove it or not. I already have existing posts that I don't want to lose as part of this switchover so I'm trying to keep that I mind as I figure out how to make this work. Where I'm really confused is what I need to change in the controller.
This is my Post Controller:
require 'pry'
class Api::V1::PostController < ApiController
before_action :authorize_user, except: [:index, :show]
# INDEX /post
def index
render json: Post.all, each_serializer: PostSerializer
end
# SHOW /post/1
def show
render json: Post.find(params[:id]), serializer: PostShowSerializer
end
# CREATE /post/new
def create
binding.pry
post = Post.new(post_params)
post.user = current_user
if post.save
render json: post
else
render json: { errors: post.errors.full_messages }
end
end
# UPDATE /post/update
def update
post = Post.find(params[:id])
if post.update(post_params)
render json: post
else
render json: { errors: post.errors.full_messages }
end
end
# DESTROY /post/destroy
def destroy
post = Post.find(params[:id])
if post.destroy
render json: {destroyed: true}
end
end
protected
def post_params
params.require(:post).permit([:title, :body, :image, :keywords])
end
def authorize_user
if !user_signed_in? || current_user.role != "admin"
redirect_to root_path
end
end
end
In the above state when I get to this part post = Post.new(post_params) I get an error saying NoMethodError (undefined method 'each' for "authorlife":String). If I remove keywords from the post_params I get this error Unpermitted parameter: :keywords
I feel like I am missing one or more steps here but it's been awhile since I've done anything with associated tables like this.
UPDATE:
Followed some of the advice below and I updated the above code to how it currently looks. Current issue is that when I check post_parms in the #create method I'm no longer receiving keywords at all. I checked the frontend and it's sending keywords. I'm assuming it's my post_params that's causing the problem. I've tried adding the keywords nested attribute like this but keywords still isn't showing up in the post_params
def post_params
params.require(:post).permit(:title, :body, :image, :keywords_attributes => [:id, :keyword])
end
This is the WIP for the code I'm trying to implement. I'm not sure what the keywords part is supposed to look like once I get the params situation figured out.
params = { post: {title: post_params["title"], body: post_params["body"], image: post_params["image"], keywords_attributes: [{ keyword: 'keyword title' },]
}}
In the above state when I get to this part post = Post.new(post_params) I get an error saying NoMethodError (undefined method 'each' for "authorlife":String). If I remove keywords from the post_params I get this error Unpermitted parameter: :keywords
You need to setup nested attributes for keywords if you want to update them through a post.
class Post < ApplicationRecord
belongs_to :user
has_and_belongs_to_many :keywords
validates :title, presence: true
validates :body, presence: true
accepts_nested_attributes_for :keywords
end
You can then pass in params structured like this in your controller
params = { post: {
title: 'title', body: "body", keywords_attributes: [
{ text: 'keyword title' },
]
}}
post = Post.create(params[:post])
https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
I already have existing posts that I don't want to lose as part of this switchover so I'm trying to keep that I mind as I figure out how to make this work.
It's good practice to remove this not used data anymore. You should write a data migration which moves the existing keywords from the posts table to the keywords table. Something like this
class KeywordsMigrator
def run
Post.all.each do |post|
keyword = Keyword.find_or_create_by(title: post.keyword)
post.keywords << keyword
end
end
end
Finally you can drop the keyword column from post.
You haven't really mentioned the current structure of the posts table so I assume you have a keyword column there. If you have a keywords column you have to name your association different until you remove the column otherwise you will run into troubles. For example you rename it to keywords_v1 and specify the class_name.
class Post < ApplicationRecord
belongs_to :user
has_and_belongs_to_many :keywords_v1, class_name: "Keyword"
validates :title, presence: true
validates :body, presence: true
end
Or you rename the column first to something like deprecated_keywords.
Related
I'm trying to learn Ruby on Rails, an I'm kinda stuck with associaton.
My project is to create a simple blog with three table. User, Post, and Comment.
In my understanding, after associationg several table with foreign key, rails would automatcily find user_id and post_id. But everytime I try to build comments, the user_id is nil.
Here's my model:
class User < ApplicationRecord
has_many :posts
has_many :comments
validates :name, presence: true, length: { minimum: 5 }, uniqueness: true
validates :password, presence: true, length: { minimum: 5 }
end
class Post < ApplicationRecord
belongs_to :user
has_many :comments
validates :title, presence: true
validates :body, presence: true, length: {minimum: 10}
end
class Comment < ApplicationRecord
belongs_to :post
belongs_to :user
validates :body, presence: true
validates :user_id, presence: true
validates :post_id, presence: true
end
Here is the screenshot when I try to create a comment:
As you can see, the post_id is not nil but the user_id is nil.
I try to input user_id manualy and it work as intended. But I can't find out how to create comment with automatic user_id and post_id.
In my understanding, after associationg several table with foreign key, rails would automatcily find user_id and post_id. But everytime I try to build comments, the user_id is nil.
There is no truth to that assumption. Rails will not automatically assign your assocations - how should it even know what user/post you want to associate the comment with?
Typically the way you would construct this is to have a nested route:
resources :posts do
resources :comments,
only: [:create]
shallow: true
end
This creates the route /posts/:post_id/comments so that we know which post the user wants to comment on - you would then adjust your forms so that it posts to the nested route:
# app/views/comments/_form.html.erb
<%= form_with(model: [post, comment]) do |f| %>
# ...
<% end %>
# app/views/posts/show.html.erb
# ....
<h2>Leave a comment</h2>
<%= render partial: 'comments/form',
locals: {
post: #post,
comment: #comment || #post.comments.new
}
%>
Getting the user who's commenting would typically be done by getting it from the session through your authentication system - in this example the authenticate_user! callback from Devise would authenticate the user and otherwise redirect to the sign in if no user is signed in.
You then simply assign the whitelisted parameters from the request body (from the form) and the user from the session:
class CommentsController
before_action :authenticate_user!
# POST /posts/1/comments
def create
# This gets the post from our nested route
#post = Post.find(params[:post_id])
#comment = #post.comments.new(comment_params) do |c|
c.user = current_user
end
if #comment.save
redirect_to #post,
status: :created
notice: 'Comment created'
else
render 'comments/show',
status: :unprocessable_entity,
notice: 'Could not create comment'
end
end
private
def comment_params
params.require(:comment)
.permit(:foo, :bar, :baz)
end
end
This is typically the part that Rails beginners struggle the most with in "Blorgh" tutorials as it introduces a resource thats "embedded" in another resource and its views and several advanced concepts. If you haven't already I read it would really recommend the Getting Started With Rails Guide.
you can create a comments as below:
user = User.find 2
post = user.posts.where(id: 2).first
comment = post.comments.build({comment_params}.merge(user_id: user.id))
Hope this will help you.
I'm building an application where I have used nested attributes to store different option records under a question record. There is a form where the user can dynamically add and remove options. Everything works fine in my create action, but in the update action, if I remove an existing option and submit the form, it is not deleted from the database.
When updating the question record, is there any way to completely overwrite the existing nested parameters and replace it with the ones we pass in the update request?
I understand that adding _destroy to the attributes and passing it as a parameter would satisfy my requirement here. Since I'm deleting the option information from my frontend state on press of a "remove" button in the UI, I'm not sending it along with the params. Is there any other method in Rails to completely overwrite nested attributes and delete those nested records which are not passed in the update request, from the update action in the controller itself?
question.rb
class Question < ApplicationRecord
belongs_to :quiz
has_many :options
validates :body, presence: true
accepts_nested_attributes_for :options
end
option.rb
class Option < ApplicationRecord
belongs_to :question
validates :body, presence: true
validates :is_correct, inclusion: { in: [ true, false ], message: "must be true or false" }
end
questions_controller.rb
class QuestionsController < ApplicationController
...
def update
#question = Question.find_by(id: params[:id])
if #question.update(question_params)
render status: :ok, json: { notice: t("question.successfully_updated") }
else
render status: :unprocessable_entity, json: { error: #question.errors.full_messages.to_sentence }
end
end
...
private
def question_params
params.require(:question).permit(:body, :quiz_id, options_attributes: [:id, :body, :is_correct])
end
Relevant question
If I understand you correctly you're deleting the options one by one by clicking a button next to the option. Thats not actually something you need or want to use nested attributes for. Nested attributes is only relevant when you're creating/editing multiple records at once.
While you can destroy a single nested record by updating the parent:
patch '/questions/1', params: {
question: { options_attributes: [{ id: 1, _destroy: true }] }
}
Its very clunky and not really a good RESTful design.
Instead you can just setup a standard destroy action:
# config/routes.rb
resources :options, only: :destroy
<%= button_to 'Destroy option', option, method: :delete %>
class OptionsController < ApplicationController
# #todo authenticate the user and
# authorize that they should be allowed to destroy the option
# DELETE /options/1
def destroy
#option = Option.find(params[:id])
#option.destroy
respond_to do |format|
format.html { redirect_to #option.question, notice: 'Option destroyed' }
format.json { head :no_content }
end
end
end
This uses the correct HTTP verb (DELETE instead of PATCH) and clearly conveys what you're doing.
I can share my recent project work which is a bit similar to your where I am using shrine gem for upload images and I can update/destroy images which is associated with a Product model
product.rb
.
.
has_many :product_images, dependent: :destroy
accepts_nested_attributes_for :product_images, allow_destroy: true
product_image.rb
.
belongs_to :product
.
_form.html.erb for update
<%= f.hidden_field(:id, value: f.object.id) %>
<%= image_tag f.object.image_url unless f.object.image_url.nil? %>
<%= f.check_box :_destroy %>
and in products controller,I have whitelisted this
product_images_attributes: [:_destroy,:image, :id]
Hope this helps you to solve on your case
In a form, I have a multiselect dropdown that I can manually sort. I override the setter method to achieve this. However, when I add validation, I get the presence error saying that "Tags cannot be blank" even when there are tags selected in the dropdown.
params: {tag_ids: ['', '1', '3']}
I think it has something to do with validating before saving which happens in the override function. Any suggestions in how I can make it to run the validations after running the setter method or vice-versa? Thanks.
Controller:
def update
if #article.update(article_params)
redirect_to articles_path
end
end
def article_params
params.require(:article).permit(:title, tag_ids: [])
end
Models:
class Article
has_many: :article_tags
has_many: :tags, through: :article_tags
validates: :tag_ids, presence: true
def tag_ids=(ids)
ids = ids.reject(&:blank?)
self.article_tags.where.not(article_tag_id: ids).destroy_all
ids.each_with_index do |id, idx|
article_tag= self.article_tags.find_or_initialize_by(article_tag_id: id)
if self.new_record?
article_tag.ordinal = idx + 1
else
article_tag.update(ordinal: idx + 1)
end
end
end
def tags
super.joins(:article_tags).order("article_tags.ordinal").distinct
end
end
class ArticleTag
belongs_to: :article
belongs_to: :tag
end
I'm creating a simple newsfeed in rails. The aim is for it to return all the posts from the groups the user is following. I am using socialization for my follow functionality.
The exact error is:
NoMethodError (undefined method `followees' for false:FalseClass)
Here are my basic models not including like and follow as they're empty:
User:
class User < ActiveRecord::Base
authenticates_with_sorcery!
attr_accessible :username, :password, :email
has_many :groups
has_many :posts
acts_as_follower
acts_as_liker
before_create :generate_auth_token
def auth_token_expired?
auth_token_expires_at < Time.now
end
def generate_auth_token(expires = nil)
self.auth_token = SecureRandom.hex(20)
self.auth_token_expires_at = expires || 1.day.from_now
end
def regenerate_auth_token!(expires = nil)
Rails.logger.info "Regenerating user auth_token"
Rails.logger.info " Expiration: #{expires}" if expires
generate_auth_token(expires)
save!
end
end
Group:
class Group < ActiveRecord::Base
attr_accessible :description, :name, :user_id
has_many :posts
belongs_to :user
acts_as_followable
end
Post:
class Post < ActiveRecord::Base
attr_accessible :body, :user_id, :group_id
belongs_to :user
belongs_to :group
acts_as_likeable
end
I have setup a function named newsfeed in my post controller. The function grabs all the groups that a user is following and then grabs all the posts that have group_ids matching group_ids in the returned groups array. But I keep getting unidentified method followees(socialization provides this). Yet it appears to work when using single users and posts in irb.
def newsfeed
#groups = current_user.followees(Group)
#posts = Post.where(:group_id => #groups)
respond_to do |format|
format.html # show.html.erb
format.json { render json: #posts }
end
end
Thanks for any help.
Apparently, your current_user method returns false, instead of a user. Check what's returned from that method, as find out why you get the error...
Your current_user return false instead of instance of User. You may see it from error text.
I have a field in my model called isTransfer:
class AddTxfrColumnsToTransaction < ActiveRecord::Migration
def change
add_column :transactions, :isTransfer, :boolean
add_column :transactions, :transferAccount_id, :integer
end
end
I create a controller that should act like action: :new, but only for a transfer call new_transfer:
def new_transfer
account = Account.find(params[:account_id])
#transaction = account.transactions.build
#transaction.description = "Transfer"
#transaction.isTransfer = true
#transaction.amount = 100
respond_to do |format|
format.html # new.html.erb
format.json { render json: #transaction }
end
end
When I see the new transfer in my view form, before posting, I can see that isTransfer is set to true. But when I post, it always goes into the DB as false. The other fields (description and amount) do not change - they go in as expected.
Here is the model:
class Transaction < ActiveRecord::Base
attr_accessible :account_id, :amount, :check, :date, :description, :is_cleared, :note, :category, :isTransfer, :transferAccount_id
validates_presence_of :amount, :date
belongs_to :account, class_name: 'Account'
belongs_to :transferAccount, class_name: 'Account'
end
I would suggest you do the presets in the create controller method rather than new
Also, you can add a "!" to make the save method return you any errors from the console: e.g
def create
###do your preset methods here
if(#transaction.save!)
end
end
Ok, this is probably a complete noob mistake. I had originally believed that if I set a value in the controller (as part of new_transfer action) that it would persist to the create action after submit. My mistake was that in not referencing it at all on the new_transfer form, it was never passed back to the Create action as a param. By adding the following to my new_transfer form, isTransfer now updates in the create action:
<%= f.hidden_field(:isTransfer) %>