Validation with override setter find_or_initialize_by - ruby-on-rails

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

Related

Split table into two associated tables

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.

Add Record to Existing Collection in a Rails App

I am attempting to develop a model in which a user can add the recipe they are viewing to an existing menu of recipes they have created, similar to adding a song to a custom playlist. I believe I have the models set up correctly (using a many to many through relationship) however I am unsure how to go about the adding of the actual records to a selected collection. Any guidance would be helpful. My code is as below.
Menus Controller
class MenusController < ApplicationController
before_action :set_search
def show
#menu = Menu.find(params[:id])
end
def new
#menu = Menu.new
end
def edit
#menu = Menu.find(params[:id])
end
def create
#menu = current_user.menus.new(menu_params)
if #menu.save
redirect_to #menu
else
render 'new'
end
end
def update
#menu = Menu.find(params[:id])
if #menu.update(menu_params)
redirect_to #menu
else
render 'edit'
end
end
def destroy
#menu = Menu.find(params[:id])
#menu.destroy
redirect_to recipes_path
end
private
def menu_params
params.require(:menu).permit(:title)
end
end
Menu Model
class Menu < ApplicationRecord
belongs_to :user
has_many :menu_recipes
has_many :recipes, through: :menu_recipes
end
menu_recipe Model
class MenuRecipe < ApplicationRecord
belongs_to :menu
belongs_to :recipe
end
Recipe Model
class Recipe < ApplicationRecord
belongs_to :user
has_one_attached :cover
has_many :menu_recipes
has_many :menus, through: :menu_recipes
end
User Model
class User < ApplicationRecord
has_secure_password
has_one_attached :profile_image
has_many :recipes
has_many :menus
end
You can do something like :
def add_recipe_to_menu
menu = current_user.menus.find params[:id]
recipe = current_user.recipes.find params[:recipe_id]
menu.recipes << recipe
end
It will add a viewing recipe to existing menu of recipes.
First make sure you build the new record off the user:
class MenusController < ApplicationController
# make sure you authenticate the user first
before_action :authenticate_user!, except: [:show, :index]
def new
#menu = current_user.menus.new
end
def create
#menu = current_user.menus.new(menu_attributes)
# ...
end
end
Then we can just add a select to the form where the user can select from his recipes:
# use form_with in Rails 5.1+
<%= form_for(#menu) do |f| %>
... other fields
<div class="field">
<%= f.label :recipe_ids %>
<%= f.collection_select :recipe_ids, f.object.user.recipies, :id, :name, multiple: true %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
f.object accesses the model instance wrapped by the form builder.
recipe_ids is a special setter/getter created by ActiveRecord for has_many associations. As you may have guesses it returns an array of ids and lets the association be set with an array of ids - automatically inserting/deleting rows in the join table in the process.
You then just need to whitelist the recipe_ids param:
def menu_attributes
params.require(:menu)
.permit(:foo, :bar, recipe_ids: [])
end
recipe_ids: [] whitelists an array of permitted scalar types. Since this is a hash option it must be listed after any positional arguments to be syntactically valid.
rb(main):003:0> params.require(:menu).permit(:foo, recipe_ids: [], :bar)
SyntaxError: (irb):3: syntax error, unexpected ')', expecting =>

Rails use an instance variable in controller or generate within a helper?

So I'm trying to build out on an Invoice page the past_due_amount where I'm trying to find only the invoices for the current account, that are not paid off, and should be in the past.
So roughly I have:
past_due_amount = Invoice.where(account: invoice.account, status: :unpaid).where('date < ? ', invoice.date).map(&:due).sum
For additional context here are the models involved:
Invoice:
class Invoice < ApplicationRecord
belongs_to :account
has_many :line_items, dependent: :destroy
has_many :payment_destinations, dependent: :destroy
has_many :prorated_fees, dependent: :nullify
enum status: [:unpaid, :paid]
validates :date, presence: true
validates :period_start, :period_end,
uniqueness: { scope: :account, allow_blank: true }, on: :create
validate :start_is_before_end
DAYS_DUE_AFTER_DATE = 14.days
scope :descending, -> { order(date: :desc) }
scope :ascending, -> { order(date: :asc) }
scope :due, -> { unpaid.where(arel_table[:date].lteq(Time.zone.today - DAYS_DUE_AFTER_DATE)) }
def total
if persisted?
line_items.sum(:amount)
else
line_items.map(&:amount).sum
end
end
end
Account:
class Account < ApplicationRecord
belongs_to :customer
belongs_to :property_address,
class_name: Address.to_s,
dependent: :destroy,
required: false
[:products, :account_changes, :equipments,
:payments, :invoices].each do |assoc|
has_many assoc, dependent: :destroy
end
accepts_nested_attributes_for :property_address
delegate :street, :city, :state, :zip,
to: :property_address, allow_nil: true
delegate :email, :full_name, to: :customer
enum status: [:staged, :active, :inactive]
scope :active_or_staged, -> { where(status: [:staged, :active]) }
scope :past_due, lambda {
joins(:invoices)
.where(
Invoice.arel_table[:status].eq(:unpaid)
.and(Invoice.arel_table[:date].lt(Time.zone.today - 14.days))
).distinct
}
scope :search, lambda { |term|
joins(:customer)
.where(
arel_table[:account_num].matches("%#{term}%")
.or(Customer.arel_search(term))
)
}
end
With the rough code in place I decided to build out a instance variable on the InvoicesController within the show method as below:
def show
#invoice = Invoice.find_by!(id: params[:id], account: current_customer.account_ids)
#account = #invoice.account
#past_due_amount = Invoice.where(account: #account, status: :unpaid).where('date < ?', #invoice.date).map(&:due).sum
end
No errors appear but that's not saying much since the examples I have are poor, at best. But my question is...should I actually be putting this in a helper instead of the show method on an InvoicesController or even in the model?
EDIT:
I've also tried putting in my Invoice model:
def self.past_due_amount
Invoice.where(account: #account, status: :unpaid).where('date < ?', #invoice.date).map(&:due).sum
end
Then in my InvoicesController:
def show
#invoice = Invoice.find_by!(id: params[:id], account: current_customer.account_ids)
#account = #invoice.account
#past_due_amount = Invoice.past_due_amount
end
End up getting undefined method `date' for #invoice.date.
The best way is to create a method past_due_amount in the InvoicesHelper
module InvoicesHelper
def past_due_amount
Invoice.where(account: #account, status: :unpaid).where('date <?', #invoice.date).map(&:due).sum
end
end
In you controller just initialize all the instance variables
def show
#invoice = Invoice.find_by!(id: params[:id], account: current_customer.account_ids)
#account = #invoice.account
end
In the view you should use: <%= past_due_amount > to show your data
Create an instance method in Account model
def past_due_amount
invoices.map(&:due).sum
end
and then from view you can all it #account.past_due_amount. no need to create extra instance variable in controller action
So I sort of used Patrick's answer but it was actually failing so I switched to passing invoice as params.
Helper
module InvoicesHelper
def past_due_amount(invoice)
Invoice.where(account: invoice.account, status: :unpaid).where('date < ?', invoice.date).map(&:due).sum
end
end
Then in my view:
<% if past_due_amount(invoice).positive? %>
<p><%= number_to_currency past_due_amount(invoice) %></p>
<% end %>

rails to_param is not working

I am following Ryan Bates railscasts video of friendly url. I am trying to implement that on my Category model by overriding the to_parammethod.
Seems like it's not working, or I am missing something.
Below is my url before overriding:
localhost:3000/search?category_id=1
After overriding the to_param the url remained same.
Following is my code:
Category model
class Category < ActiveRecord::Base
enum status: { inactive: 0, active: 1}
acts_as_nested_set
has_many :equipments, dependent: :destroy
has_many :subs_equipments, :foreign_key => "sub_category_id", :class_name => "Equipment"
has_many :wanted_equipments, dependent: :destroy
has_many :services, dependent: :destroy
validates :name, presence: true
validates_uniqueness_of :name,message: "Category with this name already exists", scope: :parent_id
scope :active, -> { where(status: 1) }
def sub_categories
Category.where(:parent_id=>self.id)
end
def to_param
"#{id} #{name}".parameterize
end
end
Controller
def search_equipments
begin
if (params.keys & ['category_id', 'sub_category', 'manufacturer', 'country', 'state', 'keyword']).present?
if params[:category_id].present?
#category = Category.active.find params[:category_id]
else
#category = Category.active.find params[:sub_category] if params[:sub_category].present?
end
#root_categories = Category.active.roots
#sub_categories = #category.children.active if params[:category_id].present?
#sub_categories ||= {}
Equipment.active.filter(params.slice(:manufacturer, :country, :state, :category_id, :sub_category, :keyword)).order("#{sort_column} #{sort_direction}, created_at desc").page(params[:page]).per(per_page_items)
else
redirect_to root_path
end
rescue Exception => e
redirect_to root_path, :notice => "Something went wrong!"
end
end
route.rb
get "/search" => 'welcome#search_equipments', as: :search_equipments
index.html.erb
The line which is generating the url
<%= search_equipments_path(:category_id => category.id ) %>
You are generating URLs in such a way as to ignore your to_param method. You're explicitly passing a value of only the ID to be used as the :category_id segment of your URLs. If you want to use your to_param-generated ID, then you need to just pass the model to the path helper:
<%= search_equipments_path(category) %>

How to set an order for association array

I' ve got a controller
class Api::V1::QuestionsController < Api::V1::BaseController
authorize_resource
before_action :set_question, only:[:show]
api :GET, '/questions/id', 'This renders question by id'
def show
render json:#question
end
and a serializer
class QuestionSerializer < ActiveModel::Serializer
attributes :id, :title, :body, :created_at, :updated_at, :short_title
has_many :answers
has_many :comments
has_many :attachments
def short_title
object.title.truncate(10)
end
end
Here is a part of test
context 'answers' do
it 'included in question object' do
expect(response.body).to have_json_size(2).at_path("answers")
end
%w(id body created_at updated_at).each do |attr|
it "contains #{attr}" do
#NO UNDERSTANDING AT ALL!!!!!
expect(response.body).to be_json_eql(answers[0].send(attr.to_sym).to_json).at_path("answers/1/#{attr}")
end
end
end
The thing is that this test passes, so obviously (for unknown reasons for me) the scope differs. I am new to rails, could anybody tell me how could I set a default scope to normalize my responds, also I have to admit that In my app I have a similar not api controller and there are no problems with that scope.
Yep this is the way. I should only set order in serializer
class QuestionSerializer < ActiveModel::Serializer
attributes :id, :title, :body, :created_at, :updated_at, :short_title
has_many :answers
has_many :comments
has_many :attachments
def comments
object.comments.order(updated_at: :asc)
end
def answers
object.answers.order(updated_at: :asc)
end
def attachments
object.attachments.order(updated_at: :asc)
end
def short_title
object.title.truncate(10)
end
end

Resources