My Rails site has different categories that the user can browse through. Right now they're rendered in the view through a simple loop, but I'd like to be able to choose the order from left to right in which they're displayed.
Here's the current code:
<h4>Explore Listings</h4>
<% #categories.each do |f| %>
<p class="category-page-category"><%= link_to f.name, view_category_path(f.id) %></p>
<% end %>
What would be an easy solution to accomplish what I want to do?
The easiest way I know of how to do this is to set up an AJAX form which will pass an order parameter which will be handled by the controller to re-render the form. Your controller would then listen for the order parameter and append that to #categories.
For example, your controller code might look something like:
def index
#categories = Category.<some_other_scope>
#categories = #categories.order(order_param) if order_param.present?
respond_to do |format
format.js { render :index }
end
end
private
def order_param
params.permit(:order)
end
First of all native sorting by the DB is to be preferred in every case (it's much faster).
If your sorting does not depend on already existing Category attributes, migrate a new attribute like position:
add_column :categories, :position, :integer
and add a scope to the model:
class Category < ActiveRecord::Base
def self.ordered default_order=nil
return scoped if default_order.nil? or
not( %w(asc desc).include?(default_order.to_s) )
order("position #{default_order}")
end
end
and then in the controller:
def index
#categories = Category.ordered params[:order]
end
following the sorting attribute approach you also should ensure your position attribute is valid defined. So like:
class Category < ActiveRecord::Base
before_create :default_position
validates :position,
presence: true,
numericality: true,
uniqueness: true,
on: :update
def self.ordered default_order=nil
return scoped if default_order.nil? or
not( %w(asc desc).include?(default_order.to_s) )
order("position #{default_order}")
end
private
def default_position
self.position = self.class.maximum("position") + 1
end
end
Changing positions has to be implemented.
Otherwise if positions never change, this means Categories have to be sorted by their creation datetime. Then you also could sort by created_at instead of position.
It sounds like you want to sort the array of categories before they are shown, no?
If so you can use http://www.ruby-doc.org/core-2.1.2/Enumerable.html#method-i-sort_by or http://www.ruby-doc.org/core-2.1.2/Enumerable.html#method-i-sort to sort your categories however you want before rendering them.
So this could end up looking something like:
<% #categories.sort_by(&:name).each do |f| %>
assuming categories have a name field. This should probably be done in the controller or in the model with a scope (if you are sorting based on a database field) to keep this logic out of the view.
looks like acts_as_list is exactly what you need
Related
ActiveAdmin generates methods to get the paths of each class. For example, if I wanted to make a link to a FooBar object I would call link_to obj.name, admin_foo_bar_path(obj).
How would I do that without needing to hard code the class of the object in admin_foo_bar_path?
For example, if I wanted to make a function usable by any class...
def show_link(obj)
display = obj.try(:name) || obj.id
link_to display, ???
end
Hello such function already exists in active admin
https://github.com/activeadmin/activeadmin/blob/ef4e80ea2f0cb528ea146becd104f7b5b029910d/lib/active_admin/view_helpers/auto_link_helper.rb#L14
example:
index do
column :name, :sortable => :name do |company|
auto_link(company)
end
column :active do |company|
company.active? ? icon(:check) : icon(:x)
end
end
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.
I have a couple models shown below and I'm using the search class method in Thing to filter records
class Category << ActiveRecord::Base
has_many :thing
end
class Thing << ActiveRecord::Base
belongs_to :category
:scope approved -> { where("approved = true") }
def self.search(query)
search_condition = "%" + query + "%"
approved.where('name LIKE ?', search_condition)
end
end
It works fine in my Things controller. The index route looks like so:
def index
if params[:search].present?
#things = Thing.search(params[:seach])
else
#thing = Thing.all
end
end
On the categories show route I display the Things for this category. I also have the search form to search within the category.
def show
#category = Categories.find(params[:id])
if params[:search].present?
#category.things = #category.things.search()
end
end
So the problem is that the category_id attribute of all the filtered things are getting set to nil when I use the search class method in the categories#show route. Why does it save it to database? I thought I would have to call #category.save or update_attribute for that. I'm still new to rails so I'm sure its something easy I'm overlooking or misread.
My current solution is to move the if statement to the view. But now I'm trying to add pages with kaminiri to it and its getting uglier.
<% if params[:search].present? %>
<% #category.things.search(params[:search]) do |thing| %>
... Show the filtered things!
<% end %>
<% else %>
<% #category.things do |thing| %>
... Show all the things!
<% end %>
<% end %>
The other solution I thought of was using an #things = #categories.things.search(params[:search]) but that means I'm duplicated things passed to the view.
Take a look at Rails guide. A has_many association creates a number of methods on the model to which collection=(objects) also belongs. According to the guide:
The collection= method makes the collection contain only the supplied
objects, by adding and deleting as appropriate.
In your example you are actually assigning all the things found using #category.things.search() to the Category which has previously been queried using Categories.find(params[:id]).
Like Yan said, "In your example you are actually assigning all the things found using #category.things.search() to the Category which has previously been queried using Categories.find(params[:id])". Validations will solve this problem.
Records are being saved as nil because you have no validations on your model. Read about active record validations.
Here's the example they provide. You want to validate presence as well because records are being created without values.
class Person < ActiveRecord::Base
validates :name, presence: true
end
Person.create(name: "John Doe").valid? # => true
Person.create(name: nil).valid? # => false
Situation:
One form that allows users to select multiple quantities of items they'd like to request
This form POSTs to two models, one parent: Request, and child: Items.
Upon submit, one Request is created, but up to several Items are created, depending on the quantity indicated
To handle this, I have two sets of params, one for the Items, one for Requests
Desired end state:
I do not want an Item to be created without a Request nor a Request to be created without the Item
All the errors present in the form (whether it's not selecting at least one item, or errors in the attributes of the Request object) are shown together to the user once the page is re-rendered; i.e., I would like to do all the error checking together
Current hacky solution & complication:
Currently, I'm checking in stages, 1) are there quantities in the Items? If not, then regardless of what the user may have put for Request attributes, the page is re-rendered (i.e., all attributes for Request are lost, as are any validation errors that would be shown). 2) Once the first stage is passed, then the model validations kicks in, and if it fails, the new page is re-rendered again
I've spent waaaaay too long thinking about this, and nothing elegant comes to mind. Happy with the hacky solution, but would love insights from much smarter people!
Controller code (fat for now, will fix later)
def create
request_params
#requestrecord = #signup_parent.requests.build
if #itemparams.blank?
#requestrecord.errors[:base] = "Please select at least one item"
render 'new'
else
#requestrecord = #signup_parent.requests.create(#requestparams)
if #requestrecord.save
items_to_be_saved = []
#itemparams.each do |item, quantity|
quantity = quantity.to_i
quantity.times do
items_to_be_saved << ({:request_id => 0, :name => item })
end
end
Item.create items_to_be_saved
flash[:success] = "Thanks!"
redirect_to action: 'success'
else
render 'new'
end
end
end
def request_params
#requestparams = params.require(:request).permit(:detail, :startdate, :enddate)
#itemparams = params["item"]
#itemparams = #transactionparams.first.reject { |k, v| (v == "0") || (v == "")}
end
And in case it's helpful, the snippet of the view code that generates the params["item"]
<% itemlist.each do |thing| %>
<%= number_field_tag "item[][#{thing}]", :quantity, min: 0, placeholder: 0 %>
<%= label_tag thing %>
</br>
<% end %>
<!-- itemlist is a variable in the controller that is populated with a list of items -->
Validations
When you mention you want all the errors to be returned at the same time, it basically means you need to use the Rails' validations functionality.
This populates the #model.errors object, which you can then use on your form like this:
<% if #model.errors.any? %>
<ul>
<% #model.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
I think your problem is you're trying to use the validations in the controller. This is both against MVC principles & generally bad for programming modularity. The functionality you require is available with the validations features:
You may benefit from using inverse_of to create some conditional validations; or using reject_if
reject_if
#app/models/request.rb
Class Request < ActiveRecord::Base
accepts_nested_attributes_for :items, reject_if: proc { |attributes| attributes['an_item_param'].blank? #-> attributes are the "item" attributes }
end
This will only be triggered if a request is created. I.E if your request fails for some reason (validation issue), the accepts_nested_attributes_for method will not run, returning your object with the appended errors
This is mainly used to validate the nested resources (I.E you can't save an item unless its title attribute is populated etc)
--
inverse_of
#app/models/request.rb
Class Request < ActiveRecord::Base
has_many :items, inverse_of: :request
accepts_nested_attributes_for :items
end
#app/models/item.rb
Class Item < ActiveRecord::Base
belongs_to :request, inverse_of: :items
validates :title, presence: true, unless: :draft?
private
def draft?
self.request.draft #-> an example we've used before :)
end
end
This is more for model-specific validations; allowing you to determine specific conditions. We use this if we want to save a draft etc
I have the following models set up:
class Contact < ActiveRecord::Base
belongs_to :band
belongs_to :mode
validates_presence_of :call, :mode
validates_associated :mode, :band
validates_presence_of :band, :if => :no_freq?
validates_presence_of :freq, :if => :no_band?
protected
def no_freq?
freq.nil?
end
def no_band?
band.nil?
end
end
class Band < ActiveRecord::Base
has_many :logs
end
class Mode < ActiveRecord::Base
has_many :logs
end
When I enter a frequency on my new view it allows for no band to be specified if a freq is entered. This creates a problem in my other views though because band is now nil. How do I allow for band not to be specified and just show up as empty on my index and show views, and then in the edit view allow one to be specified at a later point in time.
I have been able to get my index to display a blank by doing:
contact.band && contact.band.name
But I'm not sure if this is a best approach, and I'm unsure of how to apply a similar solution to my other views.
Many thanks from a rails newb!
In my views, I use the following for potentially nil objects in my views:
<%= #contact.band.name unless #contact.band.blank? %>
if your object is an array or hash, you can use the empty? function instead.
<%= unless #contacts.empty? %>
..some code
<% end %>
Hope this helps!
D
A couple years old but still a top Google result for "rails view handle nil" so I'll add my suggestion for use with Rails 3.2.3 and Ruby 1.9.3p0.
In application_helper.rb, add this:
def blank_to_nbsp(value)
value.blank? ? " ".html_safe : value
end
Then to display a value in a view, write something like this:
<%= blank_to_nbsp contact.band %>
Benefits:
"blank" catches both nil values and empty strings (details).
Simply omitting a nil object, or using an empty string, may cause formatting issues. pushes a non-breaking space into the web page and preserves formatting.
With the "if" and "unless" suggestions in other answers, you have to type each object name twice. By using a helper, you only have to type each object name once.
<%= #contact.try(:band).try(:name) %>
This will return nil if band or name do not exist as methods on their respective objects.
You can use Object#andand for this:
<%= #contact.band.andand.name %>
<%= #contact.band if #contact.band %> also works