Moving Logic to Model - ruby-on-rails

In my Rails 4 app I am starting to move my logic to the model (working on the Fat Model principle). However, I'm a little unsure on how best to tackle the basics.
My app displays bookings for the logged in user. In my BookingsController I have an action that displays the confirmed bookings:
def confirmed
#bookings = Booking.where(:status => 3, :accommodation_id => current_user.accommodation.id).order('updated_at DESC')
end
However, I have tried to move the logic to the controller so that the BookingsController now looks like this:
def confirmed
#bookings = Booking.confirmed_bookings
end
and the Booking model looks like this:
def confirmed_bookings
bookings = where(:status => 3, :accommodation_id => current_user.accommodation.id).order('updated_at DESC')
end
I am receiving an undefined method `confirmed_bookings' error and not sure what I'm doing wrong. A fix for this and some simple "starter" advice would be much appreciated.

Split your scopes in reusable chunks:
scope :confirmed, ->{ where(:status => 3) }
scope :for_user, ->(user) { where(:accommodation_id => user.accommodation.id) }
def self.confirmed_bookings_for(user)
confirmed.for_user(user).order('updated_at DESC')
end
Notice, it' would be much better not to refer to the user at all, since it's for an accommodation.

This should work if you change the confirmed_bookings method to
self.confirmed_bookings
...
end
[EDIT] Missed the part about the current user, that function will be unavailable in the model, one solution is to pass it as a parameter to the confirmed bookings function:
class Booking < ActiveRecord::Base
def self.confirmed_bookings_for (user)
where(:status => 3, :accomodation_id => user.accomodation.id).order('updated_at DESC')
end
...
end
Then in your controller you can write
#bookings = Booking.confirmed_bookings_for current_user

Related

Rails undefined method

Why is this undefined? Does it have something to do with the #current_user?
I'm trying to create tasks for my challenges. And the created task should get /achievements. However, I get a GET 500 error.
This is the error I get:
NoMethodError at /achievements
==============================
> undefined method `achievements' for #<User:0x00000105140dd8>
app/controllers/achievements_controller.rb, line 5
--------------------------------------------------
``` ruby
1 class AchievementsController < ApplicationController
2
3
4 def index
> 5 #achievements = #current_user.achievements
6 render :json => #achievements
7 end
8
9 def new 10 #achievement = Achievement.new
This is my code in my controller
class AchievementsController < ApplicationController
def index
#achievements = #current_user.achievements
render :json => #achievements
end
def new
#achievement = Achievement.new
render :json => #achievement
end
#create a new achievment and add it to the current user
#check then set the acheivments pub challenge id to the current pub challenge
def create
#achievement = Achievement.new achievement_params
#achievement.user = #current_user.id
#achievement.pub_challenge = params[:id]
if #achievement.save
# render :json => #achievement #{ status: 'ok'}
else
render :json => {:errors => #achievement.errors}
end
end
def show
#achievement = Achievement.find params[:id]
render :json => #achievement
end
def destroy
#achievement = Achievement.find params[:id]
#achievement.destroy
end
private
def achievement_params
params.require(:achievement).permit(:pub_challenges)
end
end
You are missing the has_many :achievements relation in your User model.
You'll need to create the ActiveRecord associations you require:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :achievements
end
#app/models/achievement.rb
class Achievement < ActiveRecord::Base
belongs_to :user
end
This will give you the ability to call the achievements method on any User objects you have.
Error
The error you have is described as such:
undefined method `achievements' for #<User:0x00000105140dd8>
This basically means that you're trying to call an undefined method on a User object. Might sound simple, but really, most people don't understand it.
To explain properly, you have to remember that Rails, by virtue of being built on Ruby is object orientated. This means that everything you do in Rails should be structured around objects - which are defined in your Models:
This means that each time you call an object, you're actually above to invoke a series of "methods" which will give you the ability to either manipulate the object itself, or any of the associated functionality it has.
The problem you have is that your User object doesn't have the achievements method. Whilst you could simply do the following to fix the issue, because it's Rails, you'll need to populate the record with associative data:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :achievements #-> what you need
def achievements
#this will also fix the error you see, although it's fundamentally incorrect
end
end
Something that helped me with this type of error was that the database table was missing the relevant column. Adding the required column to the database fixed the issue.

Using Active Record Reputation System gem, no sorting happens when I sort by votes

Following the RailsCast for the reputation system gem, I added the following code to my microposts_controller
def index
#microposts = Micropost.paginate(page: params[:page]).find_with_reputation(:votes, :all, order: "votes desc")
#micropost = current_user.microposts.build
end
But no sorting happens in my index action aside from the default scope I set in my model
In my micropost model I have
class Micropost < ActiveRecord::Base
belongs_to :user
has_many :retweets
has_reputation :votes, source: :user, aggregated_by: :sum
default_scope -> { order('created_at DESC') }
If I change the default scope to
default_scope -> { order('votes DESC') }
It works how I want it to for the index page only but breaks all of my other pages.
I tried removing the default scope and leaving in the find_with_reputation method but it still doesn't order by votes.
I also tried defining the scope in a method in the micropost model like this
def self.popular
find_with_reputation(:votes, :all, {:order => 'votes desc'})
end
And make the code in the microposts_controller like this
def index
#microposts = Micropost.paginate(page: params[:page]).popular
#micropost = current_user.microposts.build
end
It still does not sort by votes.
Here is a copy of the log output from visiting the micropost index page
https://gist.github.com/anonymous/9745552
Here is a link to the gem https://github.com/NARKOZ/activerecord-reputation-system/tree/rails4
My routes.rb for microposts looks like this
resources :microposts, only: [:create, :destroy, :index] do
member { post :vote }
member { post :retweet}
end
Any guidance is appreciated.
Update
My home page feed is designed differently from what I'm doing for the Micropost Index feed. Maybe comparing what works to what doesn't will help pinpoint the issue.
I have a Static Pages Controller which sets its scope for the home action like this
def home
#micropost = current_user.microposts.build
#feed_items = current_user.feed.paginate(page: params[:page])
end
In the user model I define the feed method used in the static pages controller like so
def feed
Micropost.from_users_followed_by_including_replies(self)
end
the from_users_followed_by_including_replies(self) method is a scope i set in the micropost model
scope :from_users_followed_by_including_replies, lambda { |user| followed_by_including_replies(user) }
def self.followed_by_including_replies(user)
followed_ids = %(SELECT followed_id FROM relationships
WHERE follower_id = :user_id)
where("user_id IN (#{followed_ids}) OR user_id = :user_id OR to_id = :user_id",
{ :user_id => user })
end
Maybe I need to adapt a similar approach to the Index action for the Microposts controller
EDIT
In getting my hands on the code, I've found the real problem stems from the use of default_scope.
The original order() clause specified in your default scope is still being applied, even when adding your own order().
As a side note, this issue was kind of fixed in Rails 4.0, but the behavior was reverted in 4.0.1.
The solution was to apply a reorder()
# model
def self.popular
reorder('votes desc').find_with_reputation(:votes, :all)
end
# controller
def index
#microposts = Micropost.page(params[:page]).popular
end
ORIGINAL ANSWER
It seems that using the paginate method directly may not work with activerecord-reputation-system,
However, I found some examples showing that you can use the will_paginate page and per methods:
Perhaps it will work like this:
Micropost.page(params[:page]).per(30).find_with_reputation(:votes, :all, order: "votes desc")
Or with the model scope like this:
def self.popular
find_with_reputation(:votes, :all, order: 'votes desc')
end
you could do this:
Micropost.page(params[:page]).per(30).popular
Also, as a side note, your routes file is a little strange with multiple member blocks, when only one is necessary. I would make it look like this:
resources :microposts, only: [:create, :destroy, :index] do
member do
post :vote
post :retweet
end
end

Where do I put a rails method/scope that uses params?

I have a scope that uses RubyGeocoder method, near, to filter events by location using param[:searchCity]. The param gets the user's geolocation so it shows events only near them. I currently have it working in my events_controller index action, but I also need to call it on my home page.
Considering it's a filter that gets data from the database, I thought it would be best to go in the model, but I'm finding conflicting information on whether having a param in the model is ok or bad practice. Also, I can't get it to work in the model with the param present.
What's the best practice for something like this? Where should I place the scope, the model, controller, helper, or somewhere else?
Here's my code:
Model:
class Event < ActiveRecord::Base
# attr, validates, belongs_to etc here.
scope :is_near, self.near(params[:searchCity], 20, :units => :km, :order => :distance) #doesn't work with the param, works with a "string"
end
Controller:
def index
unless params[:searchCity].present?
params[:searchCity] = request.location.city
end
#events = Event.is_near
# below works in the controller, but I don't know how to call it on the home page
# #events = Event.near(params[:searchCity], 20, :units => :km, :order => :distance)
respond_to do |format|
format.html # index.html.erb
format.json { render json: #events }
end
end
The line I'm calling in my home page that gets how many events are in the area
<%= events.is_near.size %>
Edit: Using a lambda seems to be working. Is there any reason I shouldn't do it this way?
Model:
class Event < ActiveRecord::Base
scope :is_near, lambda {|city| self.near(city, 20, :units => :km, :order => :distance)}
end
Controller:
def index
#events = Event.is_near(params[:searchCity])
...
home.html.erb
<%= events.is_near(params[:searchCity]).size %>
Accessing the params in model is not possible. Params is something which is made to exist only at controller and view level.
So best way is to write some helper method in controller to perform this.
Class Mycontroller < ApplicationController
before_action fetch_data, :only => [:index]
def fetch_data
#data = Model.find(params[:id])#use params to use fetch data from db
end
def index
end

Rails as_json with two unrelated models

Given two models and a controller:
Apples
class Apples < ActiveRecord::Base
belongs_to :not_oranges
...
def as_json(options={})
opts = {:include => [:not_oranges]}
super(options.reverse_merge! opts)
end
end
Oranges
class Oranges < ActiveRecord::Base
belongs_to :not_apples
...
def as_json(options={})
opts = {:include => [:not_apples]}
super(options.reverse_merge! opts)
end
end
Search Controller
class SearchController < ApplicationController
a = Apples.search params[:q]
o - Oranges.search params[:q]
#results = {
:apples => a,
:oranges => o
}
respond_to do |format|
format.json { render :json => #results }
end
As you can see, the two models are completely unrelated and both have different :include options in their as_json definitions.
All works as expected if the search query only hits apples or only hits oranges, but once both objects aren't empty I get:
undefined method `not_apples' for #<Oranges:0x00000004af8cd8>
Seems either the two as_json definitions are being merged, or Oranges.as_json is being overriden by Apples.as_json.
Is this expected behaviour? Is there any clean way around it without using something like RABL? I feel it would be overkill for my needs.
In pseudo code the code for hash as_json method looks like
def as_json(options={})
Hash[collect {|key,element| [key.to_s,element.as_json(options)]}]
end
But your element is modifying the options argument you pass to it. Hash is unaware of this and so passes the modified options hash to as json.
It's usually a good idea not to modify in place the arguments passed to you, except when it is very clear this is ok. I'd rewrite your method as
def as_json(options={})
defaults = {:include => :not_apples}
super(defaults.merge(options))
end

Rails 3: alias_method_chain to set specific attribute first

When user's create a post I'd like to set the user_id attribute first. I'm trying to do this using alias_method_chain on the arrtibutes method. But I'm not sure if this is right as the problem I thought this would fix is still occurring. Is this correct?
Edit:
When my users create a post they assign 'artist(s)' to belong to each post, using a virtual attribute called 'artist_tokens'. I store the relationships in an artist model and a joined table of artist_ids and post_ids called artisanships.
I'd like to to also store the user_id of whomever created the artist that belongs to their post (and I want it inside the artist model itself), so I have a user_id column on the artist model.
The problem is when I create the artist for each post and try to insert the user_id of the post creator, the user_id keeps showing as NULL. Which is highly likely because the post's user_id attribute hasn't been set yet.
I figured to get around this I needed to set the user_id attribute of the post first, then let the rest of the attributes be set as they normally are. This is where I found alias_method_chain.
post.rb
attr_reader :artist_tokens
def artist_tokens=(ids)
ids.gsub!(/CREATE_(.+?)_END/) do
Artist.create!(:name => $1, :user_id => self.user_id).id
end
self.artist_ids = ids.split(",")
end
def attributes_with_user_id_first=(attributes = {})
if attributes.include?(:user_id)
self.user_id = attributes.delete(:user_id)
end
self.attributes_without_user_id_first = attributes
end
alias_method_chain :attributes=, :user_id_first
EDIT:
class ArtistsController < ApplicationController
def index
#artists = Artist.where("name like ?", "%#{params[:q]}%")
results = #artists.map(&:attributes)
results << {:name => "Add: #{params[:q]}", :id => "CREATE_#{params[:q]}_END"}
respond_to do |format|
format.html
format.json { render :json => results }
end
end
In your controller, why not just do this:
def create
#post = Post.new :user_id => params[:post][:user_id]
#post.update_attributes params[:post]
...
end
But it seems to me that it would be much better to create the artist records after you've done validation on the post rather than when you first assign the attribute.
EDIT
I would change this to a callback like this:
class Post < ActiveRecord::Base
attr_accessor :author_tokens
def artist_tokens=(tokens)
#artist_tokens = tokens.split(',')
end
after_save :create_artists
def create_artists
#artist_tokens.each do |token|
...
end
end
end

Resources