Where should I place bestseller? method - in model or somewhere else? - ruby-on-rails-4

I've got some simple model:
class Product < ActiveRecord::Base
has_many :categories
end
Now I would like to check in some service, if product is a bestseller and do other action for it:
class ProductService
def remind
Product.all.each do |product|
puts product unless bestseller?
end
end
end
So now what is the best place to put the bestseller? method - inside model or in the service as private method?
In future it may be used in some other services or actions.
Do you think the model is right place to put this method there?
Example of bestsellers method (bestsellers are picked manualy by adding to category 'bestsellers':
def bestseller?(product)
product.categories.include?(BESTSELLER_CATEGORY_ID)
end
or
def bestseller?(product_id)
Category.find(BESTSELLER_CATEGORY_ID).products.include?(product_id)
end
I still haven't decided which one is better (both do the same)

Related

NoMethodError - new since upgrading to Rails 4

I'm at my wit's end. I upgraded to Rails 4.2.10, and everything is terrible.
Here is the relevant part of /models/product.rb
class Product < ActiveRecord::Base
delegate_attributes :price, :is_master, :to => :master
And here is /models/variant.rb:
class Variant < ActiveRecord::Base
belongs_to :product
The variants table has fields for "price" and "is_master". Products table does not.
It used to be the case that one could access Product.price and it would get/set the price for the master variant (there's really only one variant per product, the way things are currently set up).
Now it complains that:
NoMethodError: undefined method `price=' for #<Product:0x0000000d63b980>
It's true. There's no method called price=. But why wasn't this an issue before, and what on earth should I put in that method if I create it?
Here's the code to generate a product in db/seeds.rb:
product = Product.create!({
name: "Product_#{i}",
description: Faker::Lorem.sentence,
store_id: u.store.id,
master_attributes: {
listing_folder_id: uuids[i],
version_folder_id: uuids[i]
}
})
product.price = 10
product.save!
end
delegate_attributes isn't a Rails method and looks like it comes from a gem (or gems) that aren't actively maintained?
If there's a new version of whatever gem you're using that might help, because the short answer is that part of the "delegating" of an attribute would involve getting and setting the attribute, so it would generate #price= for you.
If you want to define it yourself, this should do it (within your Product class):
def price=(*args)
master.price=(*args)
end
or if you want to be more explicit:
def price=(amount)
master.price = amount
end

Assign nested attributes records to current user when using Cocoon gem

In my application I have models Post & Slides & I have:
class Post < ActiveRecord::Base
has_many :slides, inverse_of: :post
accepts_nested_attributes_for :slides, reject_if: :all_blank, allow_destroy: true
Everything works fine, only thing I need (because of how my application will work), is when a slide is created, I need to assign it to current_user or user that is creating the record.
I already have user_id in my slides table and:
class User < ActiveRecord::Base
has_many :posts
has_many :slide
end
class Slide < ActiveRecord::Base
belongs_to :user
belongs_to :post
end
My PostsController looks like this:
def new
#post = current_user.posts.build
// This is for adding a slide without user needing to click on link_to_add_association when they enter new page/action
#post.slides.build
end
def create
#post = current_user.posts.build(post_params)
respond_to do |format|
if #post.save
format.html { redirect_to #post, notice: 'Was successfully created.' }
else
format.html { render :new }
end
end
end
Any help is appreciated!
There are two ways to accomplish this:
First option: when saving the slide, fill in the user-id, but this will get pretty messy quickly. You either do it in the model in a before_save, but how do you know the current-user-id? Or do it in the controller and change the user-id if not set before saving/after saving.
There is, however, an easier option :) Using the :wrap_object option of the link_to_add_association (see doc) you can prefill the user_id in the form! So something like:
= link_to_add_association ('add slide', #form_obj, :slides,
wrap_object: Proc.new {|slide| slide.user_id = current_user.id; slide })
To be completely correct, you would also have to change your new method as follows
#post.slides.build(user_id: current_user.id)
Then of course, we have to add the user_id to the form, as a hidden field, so it is sent back to the controller, and do not forget to fix your strong parameters clause to allow setting the user_id as well :)
When I'm looking at this I see three ways to go about it, but since you're on cocoon already, I would drop the connection between user & slides - as it kind of violates good database practices (until you hit a point where you page is so popular you have to optimize of course, but that would be done differently).
You are using cocoon, but you're not utilizing the nesting of the relationship fully yet ...
The best practice would be to have cocoon's nesting create both & instead of trying to assign to current_user you call something like:
#slides = current_user.posts.find_first(param[:id]).slides
The #slides saves all the results, the .Post.find(param[:id]) finds a specific post for current_user.
Note: this is not the most optimized way & I haven't tested this, but it shows you the format of one way you can think about the relationships. You will need to hit rails console and run some tests like ...
(rails console)> #user = User.first
Next we test that there are posts available, as it's frustrating to test blanks & not get the results ...
(rails console)> #posts = #user.posts
Then we use the find method & I'm going to use Post.first just to get a working id, you can easily put "1" or any number you know is valid ...
(rails console)> #post = #posts.find(Post.first)
Finally, we go with either all slides to make sure its a valid dataset
(rails console)> #post.slides
If you want a specific slide later & have a has_many relationship just tag that find method on the .slides after.
Also one last thing - when you state earlier in there you need the current_user to be related, you can use an entry in your model.rb to create a method or a scope to get the data & allow you to link it to the current_user more easily & even drop some directed SQL query with the .where method to pull that information up if performance is an issue.
I spotted a second optimization in there ... if everything really is working - don't worry about this!
And don't forget about the strong_parameters nesting to do this fully ... Strong Param white listing
Basic format ... `.permit(:id, :something, slide_attributes: [:id, :name, :whatever, :_destroy])

param is missing or the value is empty

I have two models: Boards and Topics. I want to be able to add Topics to Boards. My nested resources are:
resources :boards do
resources :topics
end
My 'boards#show' action:
def show
#board = Board.find(params[:id])
#new_topics = Topic.all
end
which lists all posts and has a link_to:
<ul>
<%#new_topics.each do |i|%>
<li><%=i.title%> <%=link_to "Add", board_topic_path(#board,i), :method=> :put%></li>
<%end%>
</ul>
I'm also using strong_params for my Boards and Topics controller as follows:
boards_controller:
def update
#board = Board.find(params[:board_id])
#topic = Topic.find(params[:id])
if #board.update(board_params)
flash[:notice] = "Added!"
#board.topics << #topic
redirect_to boards_path
else
flash[:alert] = "Problem!"
redirect_to boards_path
end
end
...
private
def board_params
params.require(:board).permit(:name,:description)
end
topics_controller:
...
private
def topic_params
params.require(:topic).permit(:title,:body,:user_id)
end
the error message I'm getting: param is missing or the value is empty: topic.
I believe that your design is wrong.
Starting from the beginning, I would say that you have a business model Board that references one or more Topics and a Topic that is referenced by one or more Boards. So, logically you have something like this:
So, these are two independent resources, that they have a many-to-many relationship.
My model with Rails would have been:
# routes
resources :boards
resources :topics
In other words, topics are not nested resource of boards. If it were, this would mean that the topics of a board would die when the board would die. Which is not your case here, as far as I understand.
Now, since the relationship is many-to-many, then you will need a 3rd table to hold your associations (table boards and table topics are not enough). Read this on Rails Guides.
Briefly:
class Board
has_and_belongs_to_many :topics
end
class Topic
has_and_belongs_to_many :boards
end
Now, if you want to add topics to boards on your UI, then you need to have a form to edit the board. This form, besides the others, needs to have a multiple select box with the topics that would be added to the board. Then on your boards_controller#update method the param[:board] would have an attribute topic_ids[] which will automatically be used to associate the particular/selected topics to the board that you are editing. Rails does that automatically.
Note I am not inclined to be using has_and_belongs_to_many Rails association. It has a lot of limitations. You can always design your own table that will hold the many-to-many association and other extra attributes that your business model will require. For example, for each topic that is attached to a board, you might want to hold the subject, or the author. I do not know. In that case a more custom model might be needed:
class Board
has_many :board_topics
has_many :topics, through: :board_topics
end
class Topic
has_many :board_topics
has_many :boards, through: :board_topics
end
class BoardTopic
belongs_to :topic, inverse_of: :board_topics
belongs_to :board, inverse_of: :board_topics
.... add other attributes that give real business value to this association ....
end
In a RESTful situation as yours, with that link you should be hitting the update action of TopicsController with two params: board_id and id.
Try this instead:
# boards_controller.rb
def update
#board = Board.find(params[:id])
#topic = Topic.find(params[:topic_id])
if #board.update(board_params)
flash[:notice] = "Added!"
#board.topics << #topic
redirect_to boards_path
else
flash[:alert] = "Problem!"
redirect_to boards_path
end
end
# In the view
<%=link_to "Add", board_path(#board, topic_id: i.id), :method=> :put%>
Still, this is still off from any convention, as you are not updating a whole topic. You probably want to use an extra action to add a topic to a board, using the PATCH verb.

Rails 4: strong_params,nested_attributes_for and belongs_to association trouble

I really can't get my head around Rails 4 strong parameters, belongs_to association and form with fields_for.
Imagine I have model for quoting some price:
class Quote < ActiveRecord::Base
belongs_to :fee
accepts_nested_attributes_for :fee
Now, I have seeded some fees into the db, and have put some radiobuttons on my form_for #quote using fields_for. The values of the radiobuttons are simply ids of the records.
Here is the troubling part, the controller:
def create
#quote = Quote.new(quote_params)
...
end
def quote_params
params.require(:quote).permit(:amount_from, fee_attributes: [:id])
end
From my understanding, automagically Rails should fetch fee record with some id, but there is some mystic error instead.
params hash is: "quote"=>{"amount_from"=>"1200", "fee_attributes"=>{"id"=>"1"}}
Log tail:
Completed 404 Not Found in 264ms
ActiveRecord::RecordNotFound (Couldn't find Fee with ID=1 for Quote with ID=)
app/controllers/quotes_controller.rb:14:in `create'
I really don't understand what is going on here, have read Rails association guide, googled for hour for all info, but to no avail.
What I want to achieve here is to understand the correct "Rails way" to fetch some associations for new Quote object using some params I've put in the form.
Guess I got nested_attributes_for wrong, somehow thought it would call Fee.find automagically.
I've opted for ditching fields_for helpers from the form and rendering fields manually like
radio_button_tag 'fee[id]', fee.id
Then in controller I have 2 params methods now:
def quote_params
params.require(:quote).permit(:amount_from)
end
def fee_params
params.require(:fee).permit(:id)
end
And my action looks like
def create
#quote = Quote.new(quote_params)
#quote.fee = Fee.find(fee_params[:id])
...
Any additions on best practices when one has to handle lots of different objects with not so straight init logic are welcome.

How to use pundit scopes?

I have just made the switch to Pundit from CanCan. I am unsure about a couple of things, and how Pundit is best used.
For example:
If you have a resource that can have multiple parent objects, for instance lets say a Goal belongs to a student and instructor. Therefor, a student can have many goals and an instructor can have many goals. In a controller index action you might do:
if params[:student_id].present?
#account = Student.find(params[:student_id])
#goals = #account.goals
elsif params[:instructor_id].present?
#account Instructor.find(params[:instructor_id])
#goals = #account.goals
end
params are not usable inside policies, so the logic needs to be done here. I think. For what I can tell, if you skip the policy_scope you will get an unauthorized error when viewing the index page for goals.
Would you:
#goals = policy_scope(#account.goals)
OR
#goals = policy_scope(Goal.scoped).where( account_id: #account.id)
What happens when you throw a bunch of includes in the mix?
#example = policy_scoped(#school.courses.includes(:account => :user, :teacher ))
Or when needed to order...is this correct?
policy_scope(Issue.scoped).order("created_at desc")
When using scopes: What is :scope here? Is :scope an instance of the model being evaluated? I've tried accessing its attributes via :scope, but didn't work.
class Scope < Struct.new(:user, :scope)
Reading through this from a security perspective I can see a couple things that bear mentioning. For example, if you are allowing users to specify the student_id and instructor_id param fields, what's to stop them from passing in an ID for someone other than themselves? You don't ever want to let a user specify who they are, especially when you are basing policies on the users type.
For starters, I would implement Devise and add an additional boolean field called instructor that would be true when the user was an instructor but default to false for students.
Then your Users would automatically have an instructor? method defined, which will return true if the value in the instructor column is true.
You could then add a helper for students:
def student?
!instructor?
end
Now using Devise (which gives us access to a current_user variable) we can do things like current_user.instructor? which will return true if they are an instructor.
Now on to the policy itself. I just started using Pundit a few weeks ago, but this is what I'd do in your situation:
class GoalPolicy < ApplicationPolicy
class Scope < GoalPolicy
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
#scope.where(user: #user)
end
end
end
Then your (I'm assuming GoalsController class and index method) method can look like:
def index
policy_scope(Goal) # To answer your question, Goal is the scope
end
If you wanted to order you could also do
def index
policy_scope(Goal).order(:created_at)
end
I just realized that you asked this question half a year ago, but hey! Maybe it'll answer some questions other people have and maybe I'll get some feedback on my own budding Pundit skills.
Follow #Saul's recommendation on adding devise or other means of authentication.
Then you'll want to do this (Entity is Goal in your case):
#entities = policy_scope(Entity).where(...)
In entity_policy.rb:
class EntityPolicy < ApplicationPolicy
class Scope < ApplicationPolicy::Scope
def resolve
# Here you have access to `scope == Entity` and `user == current_user`
scope.where(entity: user.entity)
end
end
end
You might wonder why is where duplicated. The answer is (and here is the answer to your question): they serve different purposes. Although currently they are identical, but consider this:
You now have an admin user who has access to everything. Your policy changes:
class EntityPolicy < ApplicationPolicy
class Scope < ApplicationPolicy::Scope
def resolve
if user.admin?
scope.all
else
scope.where(entity: user.entity)
end
end
end
end
If you have organizations with goals and the following restful endpoint:
/organizations/:organization_id/goals
When a user visits /organizations/1/goals you want to make sure the user is only allowed access to goals when the user is part of the organization:
scope.where(organization: user.organization) in the policy
And you also want to make sure that when an admin visits they can only see the goals related to that organization:
policy_scope(Goal).where(organization_id: params[:organization_id]) in the controller.