What I'm thinking right now is...
I have a library full of books (entries). Each book has many checkouts (embedded document).
What I think I want to do is, upon checkout, make a new "checkout" as an embedded document. Upon checkin, I want to edit the checkout and add a "date_checked_out" field...
The issue is, my current model/controller makes a new entry each time there is a checkin or checkout...so it's doubly redundant...
What's the best way to go about this? Need more detail?
Checkout Controller:
def new
#entry = Entry.find(params[:entry_id])
#checkout = #entry.checkout.new
respond_to do |format|
format.html {render :layout => false}
end
end
def create
#entry = Entry.find(params[:entry_id])
#entry.update_attributes(:checked_out => "Out")
#checkout = #entry.checkout.create!(params[:checkout])
redirect_to "/", :notice => "Book Checked Out!"
end
class Checkout
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::MultiParameterAttributes
field :checkout_date, :type => Time
field :checkout_date_due, :type => Time
field :book_in, :type => Time, :default => Time.now
field :book_out, :type => Time, :default => Time.now
embedded_in :entries, :inverse_of => :entries
end
It makes sense the checkout would have a start and stop date. Do you need to make a checkout when a checkin occurs? You may be able to change this to an 'update' instead of a 'create' on the checkout controller - enter a checked_in_at on update.
Specifically - you'd want to be able to accept a PUT on the checkout controller - this could either be generic (allowing you to update the checkout in many ways) or specific, make a route that cleans up this for you:
resources :checkouts do
put :checkin, :on => :member
end
in checkouts_controller
def checkin
#checkout = Checkout.find(params[:id]
#checkout.update_attribute(:checked_in_at, Time.now)
# handle issues, redirect, etc.
end
Keeping it pure REST, add an update action to your Checkout controller.
Also, post your entry model. I'm assuming from your code that an entry has_one checkout, and a checkout belongs to an entry.
Something like:
*Edit because it appears OP wants to see how this works while checking for a conditional
... original boilerplate code ommitted
def update
#entry = Entry.find(params[:entry_id])
# if the book is checked out
if #entry.checked_out == "out"
# then update it
#entry.update_attributes(:checked_out => "whatever" # though I'd seriously consider changing checked out to a boolean - if it's either out or in, true or false makes sense. Ignore this advice if there are more than two states
#checkout = #entry.checkout
respond_to do |format|
if #checkout.update_attributes(:checked_out => "newValue")
...
else
.. handle errors
end
end
else
#the book does not have the correct status
respond_to do |format|
format.html { redirect_to some_action_path, :notice => "Entry is not out, so we cannot update its status." }
format.json { render json: #entry.errors, status: :unprocessible_entry }
end
end
end
Also, if you want to make the code a bit more explicit, you might consider taking swards advice and creating a few named endpoints like
def checkout
end
def checkin
end
I think that makes sense, in that someone else reading the code can very easily know exactly what that controller action is doing, as opposed to create and update.
Related
I've been using rails for a while, and my controller code starts to get out of control (no pun intended.) I've heard you want skinny controllers and fat models, and for me this hasn't come naturally in my rails progression.
I'll post one model of my rails app.
line_item #create
def create
#cart = current_cart
#product is built from base_product, after finding associated product
#base_product_id = params[:line_item][:base_product_id]
get_product_from_base_product
#line_item = #cart.line_items.build(
:product_id => #product_id,
:order_id => nil,
:weight => params[:line_item][:weight],
:quantity => params[:line_item][:quantity]
)
## Does a line item with the same product_id already exist in cart?
if #line_item.exists_in_collect?(current_cart.line_items)
#if so, change quantity, check if there's enough stock
if current_cart.where_line_item_with(#product_id).update_quantity(#line_item.quantity) == true
#line_item.destroy
redirect_to base_products_path
flash[:success] = "Detected Producted In Cart, Added #{#line_item.quantity} More to Quantity"
else
redirect_to cart_path
flash[:failure] = "Cannot Add To Cart, We Only Have #{#line_item.product.stock_qty} In Stock"
end
else
if #line_item.stock_check == true
if #line_item.save
respond_to do |format|
format.html { redirect_to base_products_path,
:notice => "(#{#line_item.quantity}) #{#line_item.product.base_product.title} Added to Cart." }
format.xml { render :xml => #line_item,
:status => :created, :location => #line_item }
end
else
format.xml { render :xml => #line_item.errors,
:status => :unprocessable_entity }
end
else
redirect_to base_products_path
if #line_item.product.stock_qty > 0
flash[:failure] = "Sorry! We Only Have #{#line_item.product.stock_qty} In Stock"
else
flash[:failure] = "Sorry! That Item Is Out Stock"
end
end
end
end
controller methods:
private
def get_product_from_base_product
#product_id = Product.where(:base_product_id => #base_product_id).where(:size_id => params[:line_item][:size]).first.id
end
LineItem model
class LineItem < ActiveRecord::Base
belongs_to :product
belongs_to :cart
after_create :set_order_weight#, :set_package_dimentions
after_update :set_order_weight#, :set_package_dimentions
#max capactiy here
def have_enough_product?
if self.product.stock_qty > self.quantity
return true
else
self.quantity = self.quantity_was
self.save
return false
end
end
def over_order_cap?
if self.quantity > 50
return true
end
end
def update_quantity(qty)
if self.have_enough_product? == true
self.quantity += qty
self.save
end
end
def stock_check
if self.product.stock_qty > self.quantity == true
return true
else
return false
end
end
def exists_in_collect?(items)
items.each do |item|
return true if self.product_id == item.product_id
end
return false
end
As you can see, this is somewhat large controller. Is this normal? All the logic is happening there. A user may already have this item in the cart, this item may not be in stock, this item has a capacity and the user is trying to order more than that. What's more, I
I know this isn't a typical stack question. Nothing is broken, but I just don't feel good about my code.
I don't want to make a 'homework' question, but I'd appreciate suggestions on how to refactor controller code with models. I'd appreciate references and suggestions.
Thanks!
update added cart.rb
class Cart < ActiveRecord::Base
has_many :line_items#, dependent: :destroy
has_one :order
def where_line_item_with(prod_id)
line_items.where(:product_id => prod_id).first
end
def sub_total
self.line_items.to_a.sum { |item| item.total_price }
end
end
I'll answer not because I'm an expert, but because I just recently went through this myself. For me, the realization came when I began putting my app under a test suite. I was having a really tough time testing my controllers actions because they did so many different things.
Here are a couple of reasons why I think you should move and separate the logic of your controller into model methods:
Reuse of code. I found that instead of writing the same code 3 or 4 different times in different controllers, I could write it once (and test it once!) in the model and call those methods in my controllers.
Testing. Rather than testing the same logic in 3 or 4 controller actions, you can test it once in the model spec (easier to test there as well). Not only does it save you time writing tests, but it removes the possibility that you implement things differently across controllers for what should be the same logic.
Readability. I'll be honest, I didn't read through your entire create action because it wasn't very readable. I had a couple similar actions, and I hated trying to debug them because it was hard to follow what was happening. Moving that logic to the model (and separating it into smaller, more specific methods) allows you to focus on the things a controller should be doing (auth, directing, handling params etc.).
So, how to refactor it? I read something in a SO question (can't find it now) that essentially said: if the logic deals with the way resources are associated, it should be in the model. I think that's a pretty good general rule. I would start there. If you don't have a test suite, I would add that simultaneously.
I was wondering whether anybody felt kind enough to help me figure out why this isn't working.
I have a Model lets call it Task which belongs to a Project Model. I basically want each Task to have a unique name per project (Project1 could have a task called task1 and so could Project2 but both could only have one called task1) . This seems to be what the :scope option is for but it doesn't seem to be working for me.
The task model is a nested resource within project and as such I call the create action via project_tasks_path(#project). It works fine creating tasks and assigning them to projects but the scope of the uniqueness validation is not taking hold. If I create a task task1 in Project1 I can't create one with the same name in task 2.
This is my setup:
Task.rb
class Task < ActiveRecord::Base
belongs_to :project
validates :name, presence: true, uniqueness: {scope: :project_id}
tasks_controller.rb
def create
#project = Project.find_by(id: params[:project_id])
#task = Task.new(model_params)
#print task to stdout
puts "#task"
ap #task
respond_to do |format|
if #task.save
flash[:notice] = "Successfully created task"
format.js
else
# no flash as form handles errors
format.js { render action: 'new' }
format.html { render action: 'new' }
end
end
end
for some reason when I output the contents of the newly created task, I get the following
#<Task:0x007ff7c7c3b178> {
:id => nil,
:name => "test",
:project_id => nil,
:created_at => nil,
:updated_at => nil
}
It seems that because project_id hasn't been set at this point it's using 'nil' as the value.
What's the best way to get around this? would it just be a custom validator?
Edit 1
def model_params
params.require(:model).permit(:name, :project_id)
end
Right, having been playing around with this, it seems that the way to make this type of validation is pretty straight forward. All it requires is that the nested resource be built in relation to it's project, this forces the :parent_id to be passed through to the validation as expected.
In the case of this toy example, that means that the create action has to look something like:
#project = Project.find_by(id: params[:project_id])
#task = #project.tasks.build(model_params)
It should be noted that because of Rails not supporting generation of nested resources from the command line, the way that the scaffold generated controllers handle creation is by Model.new(model_params) and then saving, this doesn't seem to pick up the :parent_id in time for the validation and so will need changing as above (in terms of the parent).
I'm in Rails 3. Here's my code for a method which creates Update records in response to certain attributes being changed on a model called Candidate:
before_save :check_changed, on: [:update]
def check_changed
tracked_attributes = ["period_contributions", "total_contributions",
"period_expenditures", "total_expenditures",
"debts_and_loans", "cash_on_hand",
"major_endorsements",
"rating_afl_cio",
"rating_cal_tax",
"rating_cc",
"rating_eqca",
"rating_lcv",
"rating_now"]
changes.each do |key, value|
if tracked_attributes.include?(key)
Update.create(:candidate_id => self.id, :attribute_name => key,
:new_value => value[1], :old_value => value[0])
end
end
end
The issue is that I have some rake tasks I'm running to do batch updates to the data, which end up triggering this callback unintentionally. I'd like for it only to run when a Candidate is updated from within the admin tool aka CRUD interface. Any advice on the best way to do this?
I will only use callbacks when it is something that always needs to happen, no matter the source. Magically skipping or including them normally leads to pain down the road.
My suggestion is to create a different method on the model that does the check and use that for the crud actions.
class Candidate
#...
def check_changed_and_update(attributes)
check_changed
update(attributes)
end
private
def check_changed
tracked_attributes = ["period_contributions", "total_contributions",
"period_expenditures", "total_expenditures",
"debts_and_loans", "cash_on_hand",
"major_endorsements",
"rating_afl_cio",
"rating_cal_tax",
"rating_cc",
"rating_eqca",
"rating_lcv",
"rating_now"]
changes.each do |key, value|
if tracked_attributes.include?(key)
Update.create(:candidate_id => self.id, :attribute_name => key,
:new_value => value[1], :old_value => value[0])
end
end
end
end
Then in the controller for candidate just change update to check_changed_and_update:
class CanidateController < ApplicationController
def update
#...
respond_to do |format|
if #candidate.check_changed_and_update(canidate_params)
format.html { redirect_to #candidate, notice: 'Candidate was successfully updated.' }
else
format.html { render action: 'edit' }
end
end
end
This has an added bonus of making it obvious what is going to happen when update is called.
Now you can just use the normal active record api in your rake tasks.
We recently began a compliance push at our company and are required to keep a full history of changes to our data which is currently managed in a Rails application. We've been given the OK to simply push something descriptive for every action to a log file, which is a fairly unobtrusive way to go.
My inclination is to do something like this in ApplicationController:
around_filter :set_logger_username
def set_logger_username
Thread.current["username"] = current_user.login || "guest"
yield
Thread.current["username"] = nil
end
Then create an observer that looks something like this:
class AuditObserver < ActiveRecord::Observer
observe ... #all models that need to be observed
def after_create(auditable)
AUDIT_LOG.info "[#{username}][ADD][#{auditable.class.name}][#{auditable.id}]:#{auditable.inspect}"
end
def before_update(auditable)
AUDIT_LOG.info "[#{username}][MOD][#{auditable.class.name}][#{auditable.id}]:#{auditable.changed.inspect}"
end
def before_destroy(auditable)
AUDIT_LOG.info "[#{username}][DEL][#{auditable.class.name}][#{auditable.id}]:#{auditable.inspect}"
end
def username
(Thread.current['username'] || "UNKNOWN").ljust(30)
end
end
and in general this works great, but it fails when using the "magic" <association>_ids method that is tacked to has_many :through => associations.
For instance:
# model
class MyModel
has_many :runway_models, :dependent => :destroy
has_many :runways, :through => :runway_models
end
#controller
class MyModelController < ApplicationController
# ...
# params => {:my_model => {:runways_ids => ['1', '2', '3', '5', '8']}}
def update
respond_to do |format|
if #my_model.update_attributes(params[:my_model])
flash[:notice] = 'My Model was successfully updated.'
format.html { redirect_to(#my_model) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => #my_model.errors, :status => :unprocessable_entity }
end
end
end
# ...
end
This will end up triggering the after_create when new Runway records are associated, but will not trigger the before_destroy when a RunwayModel is deleted.
My question is...
Is there a way to make it work so that it will observe those changes (and/or potentially other deletes)?
Is there a better solution that is still relatively unobtrusive?
I had a similar requirement on a recent project. I ended using the acts_as_audited gem, and it worked great for us.
In my application controller I have line like the following
audit RunWay,RunWayModel,OtherModelName
and it takes care of all the magic, it also keeps a log of all the changes that were made and who made them-- its pretty slick.
Hope it helps
Use the Vestal versions plugin for this:
Refer to this screen cast for more details. Look at the similar question answered here recently.
Vestal versions plugin is the most active plugin and it only stores delta. The delta belonging to different models are stored in one table.
class User < ActiveRecord::Base
versioned
end
# following lines of code is from the readme
>> u = User.create(:first_name => "Steve", :last_name => "Richert")
=> #<User first_name: "Steve", last_name: "Richert">
>> u.version
=> 1
>> u.update_attribute(:first_name, "Stephen")
=> true
>> u.name
=> "Stephen Richert"
>> u.version
=> 2
>> u.revert_to(10.seconds.ago)
=> 1
>> u.name
=> "Steve Richert"
>> u.version
=> 1
>> u.save
=> true
>> u.version
=> 3
Added this monkey-patch to our lib/core_extensions.rb
ActiveRecord::Associations::HasManyThroughAssociation.class_eval do
def delete_records(records)
klass = #reflection.through_reflection.klass
records.each do |associate|
klass.destroy_all(construct_join_attributes(associate))
end
end
end
It is a performance hit(!), but satisfies the requirement and considering the fact that this destroy_all doesn't get called often, it works for our needs--though I am going to check out acts_as_versioned and acts_as_audited
You could also use something like acts_as_versioned http://github.com/technoweenie/acts_as_versioned
It versions your table records and creates a copy every time something changes (like in a wiki for instance)
This would be easier to audit (show diffs in an interface etc) than a log file
Oddly enough, my model passes validation just fine, and acts as expected, however I still have the error rendered to the view.
# Controller
def up
#vote = Vote.create :vote => true, :voter => current_user, :voteable => Recipe.find(params[:id])
respond_to do |format|
format.js { render :json => {:model => 'vote', :success => #vote.valid?, :errors => #vote.errors }}
end
#vote.errors.clear # <= doesn't seem to help
end
The model I wrote has a custom validation:
class Vote < ActiveRecord::Base
# ... associations etc.
validate :voter_voting_too_frequently?
private
def voter_voting_too_frequently?
last_vote_cast_by_voter = Vote.find_last_by_voter_id self.voter
unless last_vote_cast_by_voter.nil? || last_vote_cast_by_voter.created_at < 5.seconds.ago
errors.add_to_base("You can only vote every 5 seconds.")
end
end
end
And lastly, the response that is rendered to the view: (returned as js no doubt, but would be the same if it were in a <div>)
{"errors":[["base","You can only vote every 5 seconds."]],"model":"vote","success":false}
And even though it was successful, this is continuously returned.
Ideas on how to debug this?
The issue was totally bizarre, and was related to render => :json. Strangely the :success key couldn’t be first in the hash, when the respond_to block renders json.
This was the only change that needed to be changed:
format.js { render :json => {:model => 'vote', :errors => #vote.errors.on_base, :success => #vote.errors.on_base.nil?} }
Also I'm logging a ticket with the rails team.
It doesn't look like it is passing the validation since the success status is false.
If you take a look at you custom validation it seems that the "unless" keyword has caused some confusion.
Try:
if !last_vote_cast_by_voter.nil? && last_vote_cast_by_voter.created_at < 5.seconds.ago
errors.add_to_base ...
I think your unless statement is wrong. Personally I am not used to them, so I always rewrite to if statements
if last_vote_cast_by_voter.nil? == false && last_vote_cast_by_voter.created_at > 5.seconds.ago
Looking at this line, I would suspect that last_vote_cast_by_voter.created_at should be lower than 5 seconds, therefore I suppose your unless statement should be changed into
unless last_vote_cast_by_voter.nil? || last_vote_cast_by_voter.created_at > 5.seconds.ago
Adding #vote.errors.clear indeed does not help, since the view is rendered at the point already...
If this still is not working try to write a test where you cast some votes. The time between two votes should be 1 second and 10 seconds for example.
Check if Vote.find_last_by_voter_id is working properly.
If all these test are working and rendering your view is not, then something strange is going on in your view and you should post some more information about your view, I guess.