Ordering a has_many list in view - ruby-on-rails

I have a model called Person that the user selects five personality Traits for. However, the order they pick them for matters (they are choosing most descriptive to least descriptive).
I know how to create a join table with a poison an do ordering that way. I'm using acts_as_list as well.
But I can't seem to find any help on, is how to create a way for the user of my app to set the order of the traits. That is I want to have say five select boxes on in the HTML and have them pick each one, and use something like jQuery UI Sortable to allow them to move them around if they like.
Here is a basic idea of my models (simplified for the purpose of just getting the concept).
class Person < ActiveRecord::Base
has_many :personalizations
has_many :traits, :through => :personalizations, :order => 'personalizations.position'
end
class Personalization < ActiveRecord::Base
belongs_to :person
belongs_to :trait
end
class Trait < ActiveRecord::Base
has_many :persons
has_many :persons, :through => :personalizations
end
I just have no idea how to get positioning working in my view/controller, so that when submitting the form it knows which trait goes where in the list.

After a lot of research I'll post my results up to help someone else encase they need to have list of records attached to a model via many-to-many through relationship with being able to sort the choices in the view.
Ryan Bates has a great screencast on doing sorting with existing records: http://railscasts.com/episodes/147-sortable-lists-revised
However in my case I needed to do sorting before my Person model existed.
I can easily add an association field using builder or simple_form_for makes this even easier. The result will be params contains the attribute trait_ids (since my Person has_many Traits) for each association field:
#view code (very basic example)
<%= simple_form_for #character do |f| %>
<%= (1..5).each do |i| %>
<%= f.association :traits %>
<% end %>
<% end %>
#yaml debug output
trait_ids:
- ''
- '1'
- ''
- '2'
- ''
- '3'
- ''
- '4'
- ''
- '5'
So then the question is will the order of the elements in the DOM be respected whenever the form is submitted. Specially if I implement jQuery UI draggable? I found this Will data order in post form be the same to it in web form? and I agree with the answer. As I suspected, too risky to assume the order will always be preserved. Could lead to a bug down the line even if it works in all browsers now.
Therefore after much looking I've concluded jQuery is a good solution. Along with a virtual attribute in rails to handle the custom output. After a lot of testing I gave up on using acts_as_list for what I am trying to do.
To explain this posted solution a bit. Essentially I cache changes to a virtual property. Then if that cache is set (changes were made) I verify they have selected five traits. For my purposes I am preserving the invalid/null choices so that if validation fails when they go back to the view the order will remain the same (e.g. if they skipped the middle select boxes).
Then an after_save call adds these changes to the database. Any error in after_save is still wrapped in a transaction so if any part were to error out no changes will be made. It was easiest therefore to just delete all the endowments and save the new ones (there might be a better choice here, not sure).
class Person < ActiveRecord::Base
attr_accessible :name, :ordered_traits
has_many :endowments
has_many :traits, :through => :endowments, :order => "endowments.position"
validate :verify_list_of_traits
after_save :save_endowments
def verify_list_of_traits
return true if #trait_cache.nil?
check_list = #trait_cache.compact
if check_list.nil? or check_list.size != 5
errors.add(:ordered_traits, 'must select five traits')
elsif check_list.uniq{|trait| trait.id}.size != 5
errors.add(:ordered_traits, 'traits must be unique')
end
end
def ordered_traits
list = #trait_cache unless #trait_cache.nil?
list ||= self.traits
#preserve the nil (invalid) values with '-1' placeholders
list.map {|trait| trait.nil?? '-1' : trait.id }.join(",")
end
def ordered_traits=(val)
#trait_cache = ids.split(',').map { |id| Trait.find_by_id(id) }
end
def save_endowments
return if #trait_cache.nil?
self.endowments.each { |t| t.destroy }
i = 1
for new_trait in #trait_cache
self.endowments.create!(:trait => new_trait, :position => i)
i += 1
end
end
Then with simple form I add a hidden field
<%= f.hidden :ordered_traits %>
I use jQuery to move the error and hint spans to the correct location inside
the div of five select boxes I build. Then I had a submit event handler on the form and convert the selection from the five text boxes in the order they are in the DOM to an array of comma separated numbers and set the value on the hidden field.
For completeness here is the other classes:
class Trait < ActiveRecord::Base
attr_accessible :title
has_many :endowments
has_many :people, :through => :endowments
end
class Endowment < ActiveRecord::Base
attr_accessible :person, :trait, :position
belongs_to :person
belongs_to :trait
end

Related

What is the best way to store a multi-dimensional counter_cache?

In my application I have a search_volume.rb model that looks like this:
search_volume.rb:
class SearchVolume < ApplicationRecord
# t.integer "keyword_id"
# t.integer "search_engine_id"
# t.date "date"
# t.integer "volume"
belongs_to :keyword
belongs_to :search_engine
end
keyword.rb:
class Keyword < ApplicationRecord
has_and_belongs_to_many :labels
has_many :search_volumes
end
search_engine.rb:
class SearchEngine < ApplicationRecord
belongs_to :country
belongs_to :language
end
label.rb:
class Label < ApplicationRecord
has_and_belongs_to_many :keywords
has_many :search_volumes, through: :keywords
end
On the label#index page I am trying to show the sum of search_volumes for the keywords in each label for the last month for the search_engine that the user has cookied. I am able to do this with the following:
<% #labels.each do |label| %>
<%= number_with_delimiter(label.search_volumes.where(search_engine_id: cookies[:search_engine_id]).where(date: 1.month.ago.beginning_of_month..1.month.ago.end_of_month).sum(:volume)) %>
<% end %>
This works well but I have the feeling that the above is very inefficient. With the current approach I also dind it difficult to do operations on search volumes. Most of the time I just want to know about last month's search volume.
Normally I would create a counter_cache on the keywords model to keep track of the latest search_volume, but since there are dozens of search_engines I would have to create one for each, which is also inefficient.
What's the most efficient way to store last month's search volume for all the different search engines separately?
First of all, you can optimize your current implementation by doing one single request for all involved labels like so:
# models
class SearchVolume < ApplicationRecord
# ...
# the best place for your filters!
scope :last_month, -> { where(date: 1.month.ago.beginning_of_month..1.month.ago.end_of_month) }
scope :search_engine, ->(search_engine_id) { where(search_engine_id: search_engine_id) }
end
class Label < ApplicationRecord
# ...
# returns { label_id1 => search_volumn_sum1, label_id2 => search_volumn_sum2, ... }
def self.last_month_search_volumes_per_label_report(labels, search_engine_id:)
labels.
group(:id).
left_outer_joins(:search_volumes).
merge(SearchVolume.last_month.search_engine(search_engine_id)).
pluck(:id, 'SUM(search_volumes.volume)').
to_h
end
end
# controller
class LabelsController < ApplicationController
def index
#labels = Label.all
#search_volumes_report =
Label.last_month_search_volumes_per_label_report(
#labels, search_engine_id: cookies[:search_engine_id]
)
end
end
# view
<% #labels.each do |label| %>
<%= number_with_delimiter(#search_volumes_report[label.id]) %>
<% end %>
Please note that I have not tested it with the same architecture, but with similar models I have on my local machine. It may work by adjusting a few things.
My proposed approach still is live requesting the database. If you really need to store values somewhere because you have very large datasets, I suggest two solutions:
- using materialized views you could refresh each month (scenic gem offers a good way to handle views in Rails application: https://github.com/scenic-views/scenic)
- implementing a new table with standard relations between models, that could store your calculations by ids and months and whose you could populate each month using rake tasks, then you would simply have to eager load your calculations
Please let me know your feedbacks!

Handling join table entries based on association attributes

TL;DR
What is the best way to create join table entries based on a form with the attributes of a association, like a bar code or a plate number?
Detailed explanation
In this system that records movements of items between storage places, there is a has_many_and_belongs_to_many relationship between storage_movements and storage_items because items can be moved multiple times and multiple items can be moved at once.
These items are previously created and are identified by a plate number that is physically attached to the item and recorded on its creation on the application.
The problem is that I need to create storage_movements with a form where the user inputs only the plate number of the storage_item that is being moved but I cant figure it out a way to easily do this.
I have been hitting my head against this wall for some time and the only solution that I can think of is creating nested fields on the new storage_movements form for the storage_items and use specific code on the model to create, update and delete these storage_movements by explicitly querying these plate numbers and manipulating the join table entries for these actions.
Is this the correct way of handling the problem? The main issue with this solution is that I can't seem to display validation errors on the specific plates number that are wrong (I'm using simple_forms) because I don't have storage_item objects to add errors.
Below there is a snipped of the code for the form that I'm currently using. Any help is welcome :D
# views/storage_movements/_form.html.erb
<%= simple_form_for #storage_movement do |movement_form| %>
#Other form inputs
<%= movement_form.simple_fields_for :storage_items do |item_form| %>
<%= item_form.input :plate, label: "Plate number" %>
<% end %>
<% end %>
# models/storage_movement.rb
class StorageMovement < ActiveRecord::Base
has_many_and_belongs_to_many :storage_items, inverse_of: :storage_movements, validate: true
accepts_nested_attributes_for :storage_items, allow_destroy: true
... several callbacks and validations ...
end
# models/storage_item.rb
class StorageItem < ActiveRecord::Base
has_many_and_belongs_to_many :storage_movements, inverse_of: :storage_items
... more callbacks and validations ...
end
The controllers were the default generated ones.
This was my solution, it really "feels" wrong and the validations also are not shown like I want it to... But it was what I could come up with... Hopefully it helps someone.
I created the create_from_plates and update_from_plates methods on the model to handle the create and update and updated the actions of the controller to use them.
Note: had to switch to a has_many through association due to callback necessities.
# models/storage_movement.rb
class StorageMovement < ActiveRecord::Base
has_many :movements_items, dependent: :destroy, inverse_of: :storage_movement
has_many :storage_items, through: :movements_items, inverse_of: :allocations, validate: true
accepts_nested_attributes_for :storage_items, allow_destroy: true
validate :check_plates
def StorageMovement::create_from_plates mov_attributes
attributes = mov_attributes.to_h
items_attributes = attributes.delete "items_attributes"
unless items_attributes.nil?
item_plates = items_attributes.collect {|k, h| h["plate"]}
items = StorageItem.where, plate: item_plates
end
if not items_attributes.nil? and item_plates.length == items.count
new_allocation = Allocation.new attributes
movements_items.each {|i| new_allocation.items << i}
return new_allocation
else
Allocation.new mov_attributes
end
end
def update_from_plates mov_attributes
attributes = mov_attributes.to_h
items_attributes = attributes.delete "items_attributes"
if items_attributes.nil?
self.update mov_attributes
else
transaction do
unless items_attributes.nil?
items_attributes.each do |k, item_attributes|
item = StorageItem.find_by_plate(item_attributes["plate"])
if item.nil?
self.errors.add :base, "The plate #{item_attributes["plate"]} was not found"
raise ActiveRecord::Rollback
elsif item_attributes["_destroy"] == "1" or item_attributes["_destroy"] == "true"
self.movements_items.destroy item
elsif not self.items.include? item
self.movements_items << item
end
end
end
self.update attributes
end
end
end
def check_plates
movements_items.each do |i|
i.errors.add :plate, "Plate not found" if StorageItem.find_by_plate(i.plate).nil?
end
end
... other validations and callbacks ...
end
With this, the create works as I wanted, because, in case of a error, the validation adds the error to the specific item attribute. But the update does not because it has to add the error to the base of the movement, since there is no item.

Rails 4+ has_many :through associations with unique ordering

I have two models with "has_many :through" association. It’s working good, but I need to add unique ordering for each of combination.
Let’s say we have model Train and Carriages (Railway Carriage) and each train has unique combination of carriages
# /models/train.rb
class Train < ActiveRecord::Base
has_many :carriages, through: :combinations
end
# /models/carriage.rb
class Carriage < ActiveRecord::Base
has_many :trains, through: :combinations
end
# /models/combination.rb
class Combination < ActiveRecord::Base
belongs_to :train
belongs_to :carriage
end
# /controllers/trains_controller.rb
class TrainsController < ApplicationController
def shortcut_params
params.require(:train).permit(:name, :description, carriage_ids: [])
end
end
# /views/trains/_form.html.erb
<div class="field">
<%= f.label :carriage_ids, 'Choose Carriages' %><br>
<%= f.select :carriage_ids, Carriage.all.collect { |x| [x.name, x.id] }, {}, multiple: true, size: 6 %>
</div>
For example:
train_1 = carriage_5, carriage_4, carriage_1, carriage_3, carriage_2, carriage_6
train_2 = carriage_6, carriage_5, carriage_3, carriage_1, carriage_2, carriage_4
train_3 = carriage_1, carriage_2, carriage_3, carriage_4, carriage_6, carriage_5
In this example carriage_5 have:
first place in train_1,
second place in train_2,
last place in train_3.
It’s mean I can’t use solution like this https://stackoverflow.com/a/19138677/4801165, because I don’t have parameter to order carriages.
In database I see that carriage_ids saving from 1 to 5 (from lowest to highest id), so may be there is solution to add ids one by one?
I hope there is easy solution to get correct ordering of carriage for each Train.
Thanks
You can add a position attribute to your Combination model denoting the position of the Carriage within the Train.
I tried to use Fred Willmore advice but this additional column not need if you use nested forms to adding manually each element.
You can find Gem here https://rubygems.org/gems/cocoon and use this nice guide with Standard Rails Forms https://github.com/nathanvda/cocoon/wiki/ERB-examples.

Product Model With Stock_QTY Scoped to Size

I have a functioning, self built e-com web app, but right now the app assumes we have infinite quantity.
It uses line_items and product models.
I am going to add stock_QTY as an attribute to the product
For items that don't have any variants (sizes, colors etc.), the line_item will be created if and stock_QTY is greater than one.
I'm not sure how to deal with sizes though.
Should I create different Products? IE:
Shirt.create (name:"small green shirt", color:"green", size:S, stock_QTY:4)
Shirt.create (name:"medium green shirt", color:"green", size:M, stock_QTY:6)
Shirt.create (name:"large green shirt", color: "green", size:L, stock_QTY:1)
This seems repetitive, but at least the stock QTY can have some independence. Is there a way to create only one shirt record, with variants, and allow them to have different sizes?
Ideally I'd like
Shirt.create(name:"shirt", colors:['red', 'blue', 'green'], sizes: ['s','m',l'])
and then be able to do
Shirt.where(color => "green").where(size => "L").stock_QTY
=> X number
Shirt.where(color => "green").where(size => "M").stock_QTY
=> Y number
This way I have one model, but it can store different quantities depending on the scope of the variants.
Let me know if this is unclear.
Thanks!
Update
Product.rb
require 'file_size_validator'
class Product < ActiveRecord::Base
has_many :line_items
before_destroy :ensure_not_referenced_by_any_line_item
mount_uploader :image, ImageUploader
validates :price, :numericality => {:greater_than_or_equal_to => 0.01}
validates :title, :uniqueness => true
def to_param
"#{id}_#{permalink}"
end
private
# ensure that there are no line items referencing this product
def ensure_not_referenced_by_any_line_item
if line_items.empty?
return true
else
errors.add(:base, 'Line Items present')
return false
end
end
end
Here is my Product as it is now.
from seeds.rb
Product.create!([
{
:title => "Liaisons Shirt Green",
:description => "",
:has_size => true,
:price => 24.99,
:permalink => "shirt",
:weight => 16.00,
:units => 1.00,
:image => (File.open(File.join(Rails.root, "app/assets/images/dev7logo.png")))
}
])
So, my advice is to improve the DB schema to make it more flexible and scalable ;)
Define the Size and Color models (2 new tables), make your actual Product model the BaseProduct model (just renaming the table) and finally create the Product model (new table) which will have 3 external keys (base_product_id, color_id and size_id) and of course the stock_qty field to define all possible configurations with the minimal repetition of information :)!
Just a little help, you're final classes schema should be like:
class Color < ActiveRecord::Base
end
class Size < ActiveRecord::Base
end
class BaseProduct < ActiveRecord::Base
# This will have almost all fields from your actual Product class
end
class Product < ActiveRecord::Base
# Ternary table between Color, Size and BaseProduct
end
I'm omitting all associations because I like the idea you succeed on your own, but if you need, please just ask :)
This will allows you to do BaseProduct queries like:
base_product.colors
base_product.sizes
product.base_product # retrieve the base_product from a product
and to keep trace of the quantities:
product.stock_qty
product.color
product.size # size and color are unique for a specific product
You can also create some helper method to make the creation process similar to the one you'd like to have (as shown in your question).
Well I understand the approaches you wanted to deal with. Pretty easy business logic if I understand correctly. So you wanted the following things If I get you correctly:
You have so many products
You want to add stock count record
You wanted to validate the product for selling (line items for cart) if the product available
You need to ensure if the product is already in customer's cart when you are deleting that.
So I assumed you already added the stock_qty columns.
Now you need to ensure if the product is available to be added in your cart.
So you need to write your validation in your line_item modem.
class LineItem < ActiveRecord::Base
# other business logics are here
belongs_to :product
before_validation :check_if_product_available
def check_if_product_available
# you will find your product from controller, model should be responsible to perform business
# decision on them.
if !self.try(:product).nil? && self.product.stock_qty < 1
errors.add(:product, 'This product is not available in the stock')
return false
end
end
end
This is the approach I believe is the valid way to do. And moreover, rather saving variants in same product model, I would suggest consider designing your model more efficiently with separate variant model or you can utilize the power of self association.
I hope this will help you. Let me know if I miss anything or miss interpret your problem.
Thanks

Rails: create attributes in a specific order

In my Rails app I have Users. Users are asked for their home city and district / neighborhood.
class User < ActiveRecord::Base
belongs_to :city
belongs_to :district
end
class City < ActiveRecord::Base
has_many :users
has_many :districts
end
class District < ActiveRecord::Base
has_many :users
belongs_to :city
end
In forms I build the associations using a virtual attribute on the User model that accepts a string (more info below in case it's relevant).
In the console this all works great, but in the UI it's not working. The problem seems to be, I can get city_name through a form no problem, but when I try to assign city and district in the same form it always fails. In other words, mass assignment doesn't work.
#user.update_attributes(params[:user])
Instead the only thing I have been able to figure out is to manually set each key from a form submission, like:
#user.name = params[:user][:name] if params[:user][:name]
#user.city_name = params[:user][:city_name] if params[:user][:city_name]
#user.district_name = params[:user][:district_name] if params[:user][:district_name]
This approach works, but it's a pain, kind of brittle, and feels all wrong because it starts gunking the controller up with a lot of logic.
My question is:
Is there a way to create or update attributes in a specific order, ideally in the model so that the controller doesn't have to worry about all this?
Am I doing this wrong? If so, what approach would be better.
Extra Info
Here's how I build the associations using virtual attributes on the user model, in case that's relevant to any potential answerers:
I want users to be able to select a city by just typing in a name, like "Chicago, IL". This works fine, using a virtual attribute on the user model like so:
def city_name
city.try :full_name
end
def city_name=(string)
self.city = City.find_or_create_by_location_string( string )
end
It only makes sense for a user to find or create a district from the city they've chosen. This works slightly differently:
def district_name
district.try :name
end
def district_name=(string)
if self.city.nil?
raise "Cannot assign a district without first assigning a city."
else
self.district = self.city.districts.find_or_create_by_name( string )
end
end
In the model layer these things work fine, as long as both a city_name and district_name are set the district association works as expected.
I think you could do a few things to clean this up. First, you can use delegates to clean up the code. e.g.
delegate :name, :to => :city, :prefix => true, :allow_nil => true
that way you can do something like
user = User.create
user.city_name # nil
city = City.create(:name => 'chicago')
user.city = city
user.save
user.city_name # chicago
and it will just work.
Next, I would say take the name-to-id logic out of your model. You can do it either in the form (e.g. an ajax search puts the district id/city id into a hidden field), or in the controller. Then just assign the city/district as normal.

Resources