Rails 4, Ruby 2.3.1 (noob alert)
I have a restaurant reservation app. before a reservation is saved I need to check the db to ensure there is a table available. To do this I check the date and time selected in the form with a count:
Reservation.where(r_date:params[:r_date],r_time:params[:r_time]).count < 3
assuming the restaurant has 3 tables. If the count is more than 3 then there is not a table available.
How do I get the params from #reservation in the controller to the callback function in the model?
You can use a before_save method to check availability, and invalidate the model, canceling the save, if there are no tables available.
In your controller:
class ReservationsController < ApplicationController
def create
#reservation = Reservation.new(reservation_params)
if #reservation.save
redirect_to reservation_path(#reservation)
else
render 'new'
end
end
private
def reservation_params
params[:reservation].permit(:r_date, :r_time)
end
end
Then, in your model:
class Reservation < ActiveRecord::Model
before_save :check_availability
private
def check_availability
reservation_count = Reservation.where(r_date: self.r_date, r_time: self.r_time).count
if reservation_count > 3
return false
end
end
end
I haven't had a chance to test this code, so please let me know if you have any problems.
Edit: explanation
Related
I have a rails app that I am trying to use a model file that finds # mentions in a text field and then I want it to notify the #mentioned party, using after_create callback
class Post < ApplicationRecord
after_create :notifiy_users
def notifiy_users
mentioned_users.each do |user|
Notification.create!(recipient: user,
actor: self.user,
action: 'mentioned',
notifiable: self)
end
end
def mentions
#mentions ||= begin
regex = /#([\w]+)/
matches = body.scan(regex).flatten
end
end
def mentioned_users
#mentioned_users ||= User.where(username: mentions)
end
end
on local_env this works and the notification is created and saved, but when I push to production this acts like the after_create is never called and I get no return from the notify_users method.
Any help or recommendations for a better way to handle this would be greatly appreciated.
Calling controller actions from inside a model violates MVC. Model actions should only handle data-related logic, all the actions should stay within the controller.
Instead, you should be calling the callback from within your controller:
class PostController < ApplicationController
after_action :notify_users, only: [:create]
...
def notify_users
#post.mentioned_users.each do |user|
Notification.create!(recipient: user,
actor: #post.user,
action: 'mentioned',
notifiable: self)
end
end
end
And your model will look like this:
class Post < ApplicationRecord
def mentions
#mentions ||= begin
regex = /#([\w]+)/
matches = self.body.scan(regex).flatten
end
end
def mentioned_users
#mentioned_users ||= User.where(username: self.mentions)
end
end
I have Rails 4 app with two models.
class User
has_many :bids
end
class Bid
belongs_to :user
end
A User can only create one bid per week, so I added the following to the Bid table
add_column :bids, :expiry, :datetime, default: DateTime.current.end_of_week
and the following scopes to the Bid model
scope :default, -> { order('bids.created_at DESC') }
scope :active, -> { default.where('expiry > ?', Date.today ) }
I can now prevent a User creating multiple Bids at the controller level like so:
class BidsController
def new
if current_user.bids.active.any?
flash[:notice] = "You already have an active Bid. You can edit it here."
redirect_to edit_bid_path(current_user.bids.active.last)
else
#bid = Bid.new
respond_with(#bid)
end
end
end
But what is the best approach for validating this at the model level?
I've been trying to set up a custom validation, but am struggling to see the best way to set this up so that the current_user is available to the method. Also, am I adding errors to the correct object?
class Bid
validate :validates_number_of_active_bids
def validates_number_of_active_bids
if Bid.active.where(user_id: current_user).any?
errors.add(:bid, "too much")
end
end
end
In order to maintain separation of concerns, keep the current_user knowledge out of the model layer. Your Bid model already has a user_id attribute. Also, I'd add an error like so since the validation is not checking a "bid" attribute on Bid, but rather the entire Bid may be invalid.
class Bid
validate :validates_number_of_active_bids
def validates_number_of_active_bids
if Bid.where(user_id: user_id).active.any?
errors[:base] << "A new bid cannot be created until the current one expires"
end
end
end
This seems like it should be in a collaborator service object. Create a new class that is named appropriately (something like ActiveBid, maybe think a little on the name) That class will be initialized with a current_user and either return the active bid or false.
This limits the logic for this limitation into a single place (maybe some plans in the future can have 2, etc.
Then in the controller do a before_action that enforces this logic.
before_action :enforce_bid_limits, only: [:new, create]
private
def enforce_bid_limits
active_bid = ActiveBid.new(current_user).call
if active_bid #returns false or the record of the current_bid
flash[:notice] = "You already have an active Bid. You can edit it here."
redirect_to edit_bid_path(bid)
end
end
Later on if you end up needing this logic in several places throw this stuff in a module and then you can just include it in the controllers that you want.
Using Rails 3.2. I am trying to secure my app by checking user permission on all crud actions. Here is one of the examples:
class StatesController < ApplicationController
def create
#country = Country.find(params[:country_id])
if can_edit(#country)
#state.save
else
redirect_to country_path
end
end
def destroy
#country = Country.find(params[:country_id])
if can_edit(#country)
#state = State.find(params[:id])
#state.destroy
else
redirect_to country_path
end
end
end
class ApplicationController < ActionController::Base
def is_owner?(object)
current_user == object.user
end
def can_edit?(object)
if logged_in?
is_owner?(object) || current_user.admin?
end
end
end
I notice that I have been wrapping can_edit? in many controllers. Is there a DRYer way to do this?
Note: All must include an object to check if the logged in user is the owner.
You are looking for an authorization library. You should look at CanCan which allows you to set certain rules about which objects can be accessed by particular users or groups of users.
It has a method, load_and_authorize_resource, which you can call in a given controller. So your statues controller would look like:
class StatesController < ApplicationController
load_and_authorize_resource :country
def create
#state.save
end
def destroy
#state = State.find(params[:id])
#state.destroy
end
end
CanCan will first load the country and determine whether or not the user has the right to view this resource. If not, it will raise an error (which you can rescue in that controller or ApplicationController to return an appropriate response".) You can then access #country in any controller action knowing that the user has the right to do so.
Using Rails 3.2. I have the following code:
# photo.rb
class Photo < ActiveRecord::Base
after_create :associate_current_user
private
def associate_current_user
current_user = UserSession.find.user
self.user_id = current_user.id
end
end
Before a new record is created, it searches for the current_user. This is alright if it's just 1 new record at a time. But if there are 100 records to be created, it's gonna search for the same current_user 100 times. There is definitely performance issue.
Is there a way I can cache the object for similar operation, or any suggestion?
Thanks.
you can do memoization, something as follow
class Photo < ActiveRecord::Base
extend ActiveSupport::Memoizable
after_create :associate_current_user
private
def associate_current_user
current_user = UserSession.find.user
self.user_id = current_user.id
end
associate_current_user
end
I would like to use an after_save callback to set the updated_by column to the current_user. But the current_user isn't available in the model. How should I do this?
You need to handle it in the controller. First execute the save on the model, then if successful update the record field.
Example
class MyController < ActionController::Base
def index
if record.save
record.update_attribute :updated_by, current_user.id
end
end
end
Another alternative (I prefer this one) is to create a custom method in your model that wraps the logic. For example
class Record < ActiveRecord::Base
def save_by(user)
self.updated_by = user.id
self.save
end
end
class MyController < ActionController::Base
def index
...
record.save_by(current_user)
end
end
I have implemented this monkeypatch based on Simone Carletti's advice, as far as I could tell touch only does timestamps, not the users id. Is there anything wrong with this? This is designed to work with a devise current_user.
class ActiveRecord::Base
def save_with_user(user)
self.updated_by_user = user unless user.blank?
save
end
def update_attributes_with_user(attributes, user)
self.updated_by_user = user unless user.blank?
update_attributes(attributes)
end
end
And then the create and update methods call these like so:
#foo.save_with_user(current_user)
#foo.update_attributes_with_user(params[:foo], current_user)