Rails subcategories - ruby-on-rails

How do I find and manage subcategories? (the find_subcategory method i'd defined does not seem to work.)
class CategoriesController < ApplicationController
before_action :find_category, only: [:show]
def index
#categories = Category.order(:name)
end
def show
end
private
def find_category
#category = Category.find(params[:id])
end
def find_subcategory
#subcategory = Category.children.find(params[:parent_id])
end
end
I'm using the acts_as_tree gem, which has:
root = Category.create("name" => "root")
child1 = root.children.create("name" => "child1")
subchild1 = child1.children.create("name" => "subchild1")
root.parent # => nil
child1.parent # => root
root.children # => [child1]
root.children.first.children.first # => subchild1

It's not clear what you want your find_subcategory method to do, but if you want it to find all subcategories of the category with id in params[:id], then change it to
def find_subcategories
#subcategories = Category.where(:parent_id => params[:parent_id]).all
end
In your original you're just looking for a single subcategory, and if you just want a single category you might as well just load it from it's id.

I know you accepted the answer, but I've done this before and so it might be beneficial to explain how we did it:
Firstly, we used the ancestry gem. I think acts_as_tree is deprecated -- acts_as_tree is better than ancestry, I forgot why we used it now - ancestry works in a very similar way (parent column, child methods, etc).
I'll explain our implementation with ancestry - hopefully it will give you some ideas for acts_as_tree:
#app/models/category.rb
class Category < ActiveRecord::Base
has_ancestry #-> enables the ancestry gem (in your case it should be "acts_as_tree"
end
This will allow you to populate the ancestry (in your case parent_id) column in your categories model, and (most importantly) gives you the ability to call the child methods attached to objects in the model:
#category.parent
#category.children
... etc.
--
The important thing to note here is how we're able to call the child objects (which would be subcategories in your case).
Your method is to create separate objects and have them inherit from each other. The beauty of ancestry / acts_as_tree is their added methods.
Any object with the correct parent ids can call their "children" as associative data:
In our case, we were able to associate all the objects using the ancetry column. This is slightly trickier than acts_as_tree because you have to provide the entire hierarchy in the column (which is lame), nonetheless the result is still the same:
#app/controllers/categories_controller.rb
class CategoriesController < ApplicationController
def index
#categories = Category.all
end
end
#app/views/categories/index.html.erb
<%= render #categories %>
#app/views/categories/_category.html.erb
<%= category.name %>
<%= render category.children if category.has_children? %>
This will output the sub categories for you:
How do I find and manage subcategories
You can do it like this:
#subcategories = Category.where parent_id: #category.id
or if you have your ancestry set up properly, you should be able to use the following:
#config/routes.rb
resources :categories
#app/controllers/categories_controller.rb
class CategoriesController < ApplicationController
def show
#category = Category.find params[:id]
end
end
This will allow you to use:
#app/views/categories/show.html.erb
<% #category.children.each do |subcategory| %>
<%= subcategory.name %>
<% end %>

Related

Error displaying specific database records using find_by (rails)

I am having trouble displaying specific database records in a page. I have a Course model with a :grade column as an integer, and I have created several Courses with different :grade values, and I set up a Grades controller with actions representing each Grade (from 1 - 12) so I can display the courses on separate pages depending on its grade value which was specified in the form while creating a new Course. This is how im trying to achieve this in the Grades Controller:
class GradesController < ApplicationController
def grade1
#courses = Course.find_by grade: '1'
end
def grade2
#courses = Course.find_by grade: '2'
end
.
.
.
end
The problem is that when I go to that specific Grade's page im getting the following error:
NoMethodError in Grades#grade8 undefined method `each' for
nil:NilClass
What am I doing wrong exactly? How can I only display courses that have a specific valuen on a page?
Do this:
#config/routes.rb
resources :grades, only: [] do
resources :courses, only: [:index], path: "" #-> url.com/grades/:grade_id/courses
end
#app/controllers/courses_controller.rb
class CoursesController < ApplicationController
def index
#courses = Course.where grade: params[:grade_id]
end
end
This way, you'll be able to call the following (url.com/grades/5):
#app/views/courses/index.html.erb
<% #courses.each do |course| %>
<%= course.title %>
<% end %>
The main problem you have is that you're expecting find_by to return more than one result. As per the docs:
Finds the first record
What you need is .where:
#courses = Course.where grade: "1"
--
The second thing you need to understand is that Ruby is object orientated.
This is important because you're calling actions which are super specific:
def grade1
This is VERY bad practice -- it cuts out any extensibility and makes it so that your application can only work in the prescribed manner.
The way around this is to use the object-orientated Restful methods built into Rails, namely show or index:
def show
#courses = Course.where grade: params[:grade_id]
end

Rails 4 Stack level too deep Error

I'm creating a marketplace app where sellers can list items to sell. I want to create a category dropdown so customers can select a category to shop.
In my listing model, I have a 'category' field. When a user selects a category, I want the view to filter listings from that category.
In my routes.rb:
get '/listings/c/:category' => 'listings#category', as: 'category'
To create the category menu - in my index.html.erb:
<%= Listing.uniq.pluck(:category).each do |category| %>
<%= link_to category, category_path(category: category) %>
<% end %>
In my listings controller:
def category
#category = category
#listings = Listing.not_expired.where(:category => #category)
end
category.html.erb:
<% #listings.each do |listing| %>
#some html
<% end %>
The homepage category menu shows up. The routes are created. But when I click on the category, the url such as listings/c/necklaces gives me the stack level too deep error.
FYI "Stack Level Too Deep" basically means you have an infinite loop in your code somewhere
--
From what I can see, the error will be here:
def category
#category = category
With this code, you're basically invoking the category method again, which in turn will invoke the category method etc, in a never-ending cycle. This will prevent your application from being able to run without reloading itself in infinite recursion.
You should change it to:
def category
#category = params[:category]
#listings = Listing.not_expired.where(:category => #category)
end
However, a much more refined way would be:
#app/models/category.rb
class Category < ActiveRecord::Base
has_many :listings do
def not_available
#your not available method here
end
end
end
#app/models/listing.rb
class Listing < ActiveRecord::Base
belongs_to :category
end
#app/controllers/your_controller.rb
def category
#category = Category.find params[:categpry]
#listings = #category.listings.not_available

How can I capture an instance generically?

I'm using Rails 3.2.19 and Ruby 2.1.2. I've been googling around trying to figure this out, but perhaps I'm not searching for the right thing. Anyway, I'll try and be as concise as possible.
I have a few different models that all have a name attribute. In my views I want to somehow be able to access that name attribute regardless of the instance name passed into the view. Currently my various controllers create instances of their respective models. For instance:
class PagesController < ApplicationController
def show
#page = Page.find(params[:id])
respond_to do |format|
format.html
end
end
end
-
class ProductsController < ApplicationController
def show
#product = Product.find(params[:id])
respond_to do |format|
format.html
end
end
end
While I understand I could simply re-name the instances something generic, I was wondering if there was some way of accessing any/all instances while maintaining unambiguous instance names.
Basically something like this:
page.html.haml
%h1= resources[0].name #equates to #page.name
%h2= #page.some_other_attribute
or
product.html.haml
%h1= resources[0].name #equates to #product.name
%h2= #product.price
Where in each of the above resources[0] would be either #page or #product
You will have to define a route with an additional resource_type parameter to a generic controller or otherwise just include the resource_type into the url query parameter
/resources/product/17
or
/resources/17?resource_type=product
This will allow you to do the following in the controller
class ResourcesController < ApplicationController
def show
#resource = find_resource(params)
respond_to do |format|
format.html
end
end
private
def find_resource(params)
resource_klass = {
product: Product,
page: Page
}[params[:resource_type]]
resource_klass.find(params[:id])
end
end
Another Option would be to introduce another ResourceType Entity and define a polymorphic :has_one :belongs_to association to the actual resource entity (product, page). Then always search for ResourceTypes and load the polymorphic resource entity
class ResourceType < ActiveRecord::Base
belongs_to :resource, polymorphic: true
end
class Product < ActiveRecord::Base
has_one :resource_type, as: :resource
end
class Page < ActiveRecord::Base
has_one :resource_type, as: :resource
end
product_resource_type = ResourceType.create(...)
product = Product.create(resource_type: product_resource_type)
page_resource_type = ResourceType.create(...)
page = Page.create(resource_type: page_resource_type)
ResourceType.find(product_resource_type.id).resource
=> product
ResourceType.find(page_resource_type.id).resource
=> page
I figured this out after discovering instance_variables and instance_variables_get
Those methods will return all instance variables being passed into the view. From there I discovered that the :#_assigns instance variable contained the instances that I was looking for. So I iterated over them to find if any had the name attribute.
- instance_variable_get(:#_assigns).each do |var|
- if var[1].respond_to?("name")
%h1= var[1].name
There is probably a better way of accomplishing this, so if anyone has any opinions, they are welcome.

rails create page from

I have Categories and Products. A product has a relation
belongs_to :category
In the categories show page I have a button to add a new product. This button goes to a page where I create the new product, but I need to give the category to the new product.
How can I pass the id from the category page where I was to the new Product? So, if I am in the category Electronic I click 'Add product' and this product automaticaly is associated with Eletronic category.
Hope you can understand what I want.
Thanks
You need to pass the category_id in your link, e.g. new_product_path(category_id: #category.id).
You will also need to have a field in your product form to save the category's ID, e.g <%= f.hidden_field :category_id, params[:category_id] %>
First, I would decide whether each product is contained within a category, or whether it's simply associated with a category. Hints it is contained would be:
You expect each product to have exactly one 'parent' category.
You expect each product will always appear in the context of its parent category.
If and only if you believe this to be the case, I would be tempted to nest the product resource within the category.
# routes.rb
resources :categories do
resources :products
end
# products_controller.rb (SIMPLIFIED!)
class ProductController < ApplicationController
before_filter :get_category
def new
#product = #category.products.build
end
def create
#product = #category.products.build(params[:product])
if #product.save
redirect_to #product
else
render template: "new"
end
end
def get_category
#category = Category.find(params[:category_id])
end
end
If you do this, rails will ensure your product is associated with the right category. The magic happens in #category.products.build, which automatically sets the category_id based on the relationship.
If you'd rather keep categories and products as simple associations, I'd just use a query parameter as per Eric Andres answer, although I'd be tempted to handle it in a slightly different way:
# link:
new_product_path(category_id: #category.id) # So far, so similar.
# products_controller.rb
class ProductsController < ApplicationController
def new
#product = Product.new
#product.category_id = params[:category_id].to_i if params[:category_id]
end
end
# new.erb
<%= f.hidden_field :category_id %>
This is mostly just a stylistic difference. Eric's answer will work too - I just prefer to set the value on the model itself rather than have the view worry about parameters etc.

How to define a controller function that works for instances of any model

Implementing versioning for a Rails app I'd like to have a view that displays all versions of a model with some extra functionality like reverting etc.
I use the paper_trail gem for the versioning.
I know that I could do that by writing a controller function like versions and a view for every model but I'd like to do it for all models at once. This should be possible because the model.versions attribute is always structured identically.
Ideally the URL should look like /pages/testpage/versions while testpage is the page id.
This seems similar to the concept of nested routes in rails.
resources :pages do
resources :versions
end
The problems with nested routes however are:
Needs extra configuration per model
I cannot access the testpage object without knowing of which model it is an instance.
I also wasn't able to find a way to determine the model since the only thing that is provided to my versions controller is the params hash.
I'm completely open to alternative solutions that might not follow my initial ideas.
Write it in your ApplicationController and define it as a helper_method.
For example
class ApplicationController < ActionController::Base
helper_method :current_time
def current_time
Time.now
end
end
Now you can cal current_time everywhere in controllers or views.
Also you can write separate Module/Class and define there your helpers methods. Than you should include this file into your ApplicationController as well
UPD after theme is changed
I didn't think about your actual question. But I can say that your approach is nod the best here.
You should create new resource instead of creating new functionality which will hard to be tested. So create new resource (controller): versions and play around this controller.
For example how it can work:
/versions/pages/132
/versions/comments/1003
How to realize it:
match "/versions/:model/:id", :to => "versions#index"
In your controller:
class VersionsController < ActionController::Base
def index
#object = my_type.find(params[:id])
#versions = #object.versions
end
private
def my_type
params[:model].constantize
end
end
Of course you can change routes the way you want:
match "/:model/:id/versions", :to => "versions#show"
So now your pretty /pages/testpage/versions will work fine for you without any new strange logic.
UPD 2
Imagine you have got this route:
match "/:model/:id/versions", :to => "versions#index", :as => :versions
And this objects:
#page = Page.last
#hotel = Hotel.find(123)
#comment = #page.comments.first
How will we create links for versions:
<%= link_to "Versions of this page", versions_path(:model => #page.class.to_s, :id => #page.id) %>
<%= link_to "Versions of this hotel", versions_path(:model => #hotel.class.to_s, :id => #hotel.id) %>
<%= link_to "Versions of this comment", versions_path(:model => #comment.class.to_s, :id => #comment.id) %>
I would suggest passing a param such as 'type' and stuff the model name there. Then in your controller you can do:
class VersionsController < ApplicationController
def index
model = params[:type].classify.constantize
#obj = model.find(params[:id])
end
end
For your links, you can pass queries to the link_to helper
<%= link_to versions_path(#model, :type => #model.class) %>
Or something along those lines.

Resources