I build a little e-commerce solution. At the end of an order, I want the cart to delete, so a user can't access the same cart after ordering. After I render the order_confirmation page, I have an after_action [:show] that deletes the cart. If refreshed, the order confirmation page won't work because the line items(items teh user bought) were destroyed with the cart.
This is fine, I already sent them an email and if they refresh the confirmation page I'd like an activerecord::recordnotfound rescue.
my view looks like this though
<% #order.items.each do |id| %>
<% #line_item = LineItem.find(id) %>
(line item data to show)
When refreshed, the line_items delete and I get an activerecord error.
Couldn't find LineItem with id=8
Since this is being called from the view, and isn't exactly conventional -- how can I rescue to redirect and say something like "Your Cart is Reset" or something along those lines? I tried putting it in the controller but it didn't get touched (or didn't trigger..)
after_action :remove_cart, only: [:show]
def index
#orders = Order.all
end
def show
#order = Order.find(params[:id])
#cart = current_cart
rescue ActiveRecord::RecordNotFound
logger.error "User Refresh Page"
redirect_to products_path
raise
end
If it's super advised not to have a loop like that in the view, should create an array in the controller and then loop through them on the view and create the rescue in the controller?
UPDATE:
remove_cart method is pretty blunt, looks like this.
def remove_cart
#cart = current_cart
#cart.destroy
end
Update
I've followed Damien's instructions but the after commit
gives me this
(6.2ms) COMMIT
undefined method `true=' for #<Cart:0x00000106b9fb98>
Redirected to http://localhost:3000/orders/8
Completed 302 Found in 1933ms (ActiveRecord: 19.4ms)
with order.rb
after_commit :archive_cart, on: :create
private
def archive_cart
cart.archive!
end
and cart.rb as
def archive!
update_attribute(active, false)
end
This is based on our running conversation, here:
https://chat.stackoverflow.com/rooms/55591/discussion-between-peege151-and-damien-roche
1. Update your Order model to include:
class Order
belongs_to :cart
has_many :line_items, through: :cart
end
Then, in your view:
<% #order.line_items.each do |line_item| %>
2. As meager noted, you should archive order details. Add a status to your cart, such as:
add_column :carts, :active, :boolean, default: true
Then, instead of #cart = current_cart in your controller, refer to the cart directly via #order.cart.
3. Move destroy/archive_cart logic into your models
Note: this is not normal behaviour -- Order would usually have a status, and the cart would be archived when that Order is confirmed, but asker is using a preliminary model (OrderPreview), where each new Order is pre-confirmed.
class Order
after_commit :archive_cart, on: :create
private
def archive_cart
cart.archive!
end
end
class Cart
def archive!
update_attribute(:active, false)
end
end
Related
I want my trade page to show a list of every Item that a user has added to their cart, and I'm having trouble understanding why this implementation is giving me a NoMethodError
So in the #show action of my TradesController I have a trade_ids variable that contains an array of added items returned by $redis.smembers current_user_trade. I then use this to perform a lookup on the id of each item, and loop through the instance variable in my view.
My Trades Controller:
class TradesController < ApplicationController
def show
trade_ids = $redis.smembers current_user_trade
#trade_items = Item.find(trade_ids)
end
def add
$redis.sadd current_user_trade, params[:item_id]
render json: current_user.trade_count, status: 200
end
def remove
$redis.srem current_user_trade, params[:item_id]
render json: current_user.trade_count, status: 200
end
private
def current_user_trade
"trade#{current_user.id}"
end
end
Here's the method I'm using to add items to current_user_trade:
class Item < ActiveRecord::Base
extend FriendlyId
friendly_id :slug, use: [:slugged, :finders]
def slug
[artist.parameterize, title.parameterize].join("-")
end
def trade_action(current_user_id)
if $redis.sismember "trade#{current_user_id}", id
"Remove from"
else
"Add to"
end
end
end
My routes:
resource :trade, only: [:show] do
put 'add/:item_id', to: 'trades#add', as: :add_to
put 'remove/:item_id', to: 'trades#remove', as: :remove_from
end
And then in my trades view I've got a basic:
<% #trade_items.each do |item| %>
<p><%= item.title %></p>
etc
<% end %>
My initial thought is that this had something to do with the fact that I've been using FriendlyId for slug generation. But according to FriendlyId's doc, adding :finders to the slug declaration in my Item model should reenable lookups by id. But it's not.
I also tried refactoring this so that it used my Items :slug, but that too was to no avail.
Fixed it. Turns out it was cached. I switched from storing item_id to item_slug halfway through, and needed to run redis-cli flushall to get it to store and access the right data.
I have three models...
models/resident.rb
class Resident < ActiveRecord::Base
belongs_to :hostel
has_many :leaves,dependent: :delete_all
has_one :user,dependent: :delete
end
models/user.rb
class User < ActiveRecord::Base
belongs_to :resident
end
models/leave.rb
class Leave < ActiveRecord::Base
belongs_to :resident
end
Now when I am trying to access the value of leave's attribute in views/leave/show.html.erb
I am getting this:
app/views/leaves/show.html.erb
<%= #leaves %>
out put In Browser :
#<Leave::ActiveRecord_Associations_CollectionProxy:0x007fde611850f0>
My leave controller looks like :
leaves_controller.rb
class LeavesController < ApplicationController
def new
if logged_in?
#leave=Leave.new
else
flash[:info]="Please login to mark a leave"
redirect_to root_path
end
end
def show
#leaves= current_user.resident.leaves
end
def create
#leave=current_user.resident.leaves.create(leave_params)
if #leave.save
flash[:info] = "Leave successfully marked"
redirect_to new_leave_path
else
flash[:danger] = "Something wrong Happened try again"
redirect_to root_path
end
end
private
def leave_params
params.require(:leave).permit(:start_date,:end_date,:destination)
end
end
Am I making correct leaves for resident and related user (create method)?
Is show method correct ?
and How to assess the user's leaves attribute in show.html.erb of leaves views.
A Resident has_many Leaves so current_resident.leaves returns an array of all the current_resident's leaves. You will need to loop through leaves to show individual attributes. Try
#leaves.first.attribute_name
in your view to get an idea of how the data is represented. To show all the leaves you'll need to use a loop in the view
#leaves.each do |leave|
leave.inspect
end
You are doing everything fine, and show method is fine, and the template shows exactly what is was told to show.
#leaves is a collection. You probably want to show itโs elements? This should lead to the proper solution:
<% #leaves.each do |l| %>
<%= l.inspect %>
<% end %>
def completed_offer
redirect_to accepts_thankyou_path
#checkout.destroy
end
Above is a redirection in Rails 4. I understand that the code below the redirect will continue which I am fine with. This is a checkout routine and I destroy #checkout at the end which is a "cart" with associated "cart_items". Below is the association which causes all the cart_items to be destroyed on a call to destroy a cart
class Cart < ActiveRecord::Base
has_many :cart_items, :dependent => :destroy
Is there anyway to render a view before continuing the checkout routine so I can have access to the cart AND cart_items? I can't render and have to redirect because I don't want the user to keep resubmitting a post-data on a refresh.
The resolution was to do;
#cart = Cart.find('something')
#items = #cart.cart_items.to_a
#cart.destroy
In this way I had access to both #cart and #items for a single view
I'm running into a perplexing issue that I can only resolve partway, and hopefully, someone more experienced can tell me whether I can achieve what I wish, or if I'm barking up the wrong tree.
I have a Rails 4 application which uses Devise and CanCan. I'd like to make a small subset of application functionality available to guest users (not logged in). I can achieve this by specifying a get route to a controller method and using link_to to reach that method. I cannot, however, figure out how to get the value of a select box to pass along as parameters on that page without making that view a form using form_tag (there is no model associated with this view).
I can pass hardcoded params along like so:
<%= link_to "Month", activities_search_month_path(:resource_id => 4) %>
but I'd rather have something like:
<%= link_to "Month", activities_search_month_path(:foo => :resource_id) %>
where the second symbol refers to the value of a select_tag. This second example delivers a literal value of "resource_id" when I dump the :foo key unless I convert my view to a form.
If I turn the view into a form by enclosing all the erb in a form_tag, I get a 401 Forbidden error, after which the Devise sign in form is rendered. My guess is that any time you want to process a form, Rails (or Devise) demands authentication on some level. The behavior is the same when I use button_to rather than link_to, since button_to wraps itself in a form under the covers.
How can I set that resource_id argument in my link_to, or will I be forced to create a guest user access level and silently log in guest users? It's important for the UX that users can access this functionality with the least amount of effort possible.
Thanks in advance.
Addendum: quick_search method from controller
def quick_search
puts "quick search 0"
if(params[:time_period] == 'today')
#resource = Resource.find(params[:resource_id])
#site = Site.find(params[:site_id])
#time_period_string = "Activities for #{localize_date(Date.today)} at #{#resource.name}, #{#site.name}"
puts "quick search 1"
if user_signed_in?
puts "quick search 2a"
#activities = Activity.where("system_id = ? and start_date = ? and activity_status_id = ? and resource_id = ?", current_system_id, #today, 2, params[:resource_id])
else
puts "quick search 2b"
if(Setting["#{current_subdomain_not_signed_in}.quick_search_guest_access"] == 'true')
puts "quick search 3a"
current_system_id = current_system_id_not_signed_in
#activities = Activity.where("system_id = ? and start_date = ? and activity_status_id = ? and resource_id = ?", current_system_id, #today, 2, params[:resource_id])
else
puts "quick search 3b"
redirect_to '/users/sign_in'
end
end
end
Note: the quick_search method is never entered. CanCan (or maybe Devise) steps in immediately and redirects to sign in:
Console output:
Started GET "/activities/quick_search" for 127.0.0.1 at 2015-04-12 18:01:58 -0700
Processing by ActivitiesController#quick_search as HTML
(0.2ms) SELECT DISTINCT "systems"."subdomain" FROM "systems"
Completed 401 Unauthorized in 1ms
Started GET "/users/sign_in" for 127.0.0.1 at 2015-04-12 18:01:58 -0700
Processing by Devise::SessionsController#new as HTML
(0.2ms) SELECT DISTINCT "systems"."subdomain" FROM "systems"
Rendered layouts/_header.html.erb (0.8ms)
Rendered devise/shared/_links.html.erb (4.1ms)
Rendered devise/sessions/new.html.erb within layouts/application (14.7ms)
Rendered layouts/_footer.html.erb (0.0ms)
Completed 200 OK in 285ms (Views: 282.3ms | ActiveRecord: 0.2ms)
Ability.rb
can :quick_search, Activity
can :search_day, Activity
can :search_week, Activity
can :search_month, Activity
The odd thing is that link_to quick_search fails with a 401, but link_to the other three methods works fine -- I just can't get parameters to them dynamically.
If you are using CanCan(Can?) you can define a special ability for guests.
How does your Ability-model look?
Which controller are handling the action that you want to view?
How do you authenticate with CanCan in this controller?
https://github.com/CanCanCommunity/cancancan/wiki/CanCan-2.0
Under the "Defining Abilities" you can see a non-user example.
Fixing CanCan is probably the best option, if you do not want to:
For the part with the link and select box it would be easiest to handle as a form and then handle the redirect in the controller, it could also be done with a remote ajax form.
http://edgeguides.rubyonrails.org/working_with_javascript_in_rails.html
This should work:
<% form_tag Activity, activity_quick_search_path, remote: true do %>
<%= select_tag :resource_id...%>
<%= submit_tag %>
<%end%>
Edit after comments:
The culprit here is(was) an:
before_action :authenticate_user!
Causing Devise to redirect to sign in page.
However, if you have CanCan you shouldn't need the authenticate_user.
Short example:
With only Devise I would do:
class NewsController < ApplicationController
before_action :authenticate_user!
before_action :set_news, except: [ :index, :new ]
def index
#news = News.all
end
def show
end
def new
#news = News.new
end
def edit
end
def create
#news = News.new(news_params)
flash[:notice] = 'News created' if #news.save!
redirect_to #news
end
def update
#news.update! news_params
redirect_to #news
end
def destroy
#news.destroy!
redirect_to News
end
private
def news_params
params.require(:news).permit(some_attributes)
end
def set_news
#news = News.find(params[:id])
end
end
How it looks with CanCanCan:
class NewsController < ApplicationController
load_and_authorize_resource
def index
end
def show
end
def new
end
def edit
end
def create
flash[:notice] = 'News created' if #news.save!
redirect_to #news
end
def update
#news.update! news_params
redirect_to #news
end
def destroy
#news.destroy!
redirect_to News
end
private
def news_params
params.require(:news).permit(some_attributes)
end
end
Which I find super neat ๐
Hope that this can help as well.
I'm working on a page that displays a restaurant menu. I have 2 models: FoodMenu has_many :products and Product belongs_to :food_menu. I don't have controllers for either model. Instead, I am using a "pages_controller.rb" to display each FoodMenu and its Products with a "menus" action:
def menus
#food_menus = FoodMenu.includes(:products).all
end
I want to use Action Caching for the menus page (localhost:3000/menus), which is working, but I can't get the cache to expire when I update, create, or destroy a product.
At the top of "pages_controller.rb", I have:
caches_action :menus
cache_sweeper :pages_sweeper
I tried creating separate sweepers for the Product and FoodMenu models in app/sweepers using the example code here: http://guides.rubyonrails.org/caching_with_rails.html#sweepers, but that didn't work. Then, I read in a SO entry that the sweeper is supposed to observe all the models that the controller uses, so I assumed that meant I have to create a "pages_sweeper.rb"
that observes both the Product and FoodMenu models and expires the "menus" action. That didn't work either. What am I doing wrong? Here is what I have right now in "pages_sweeper.rb":
class PagesSweeper < ActionController::Caching::Sweeper
observe Product, FoodMenu
# If our sweeper detects that a Product was created call this
def after_create(product)
expire_cache_for(product)
end
# If our sweeper detects that a Product was updated call this
def after_update(product)
expire_cache_for(product)
end
# If our sweeper detects that a Product was deleted call this
def after_destroy(product)
expire_cache_for(product)
end
def after_create(food_menu)
expire_cache_for(food_menu)
end
# If our sweeper detects that a FoodMenu was updated call this
def after_update(food_menu)
expire_cache_for(food_menu)
end
# If our sweeper detects that a FoodMenu was deleted call this
def after_destroy(food_menu)
expire_cache_for(food_menu)
end
private
def expire_cache_for(product)
# Expire the menus action now that we added a new product
expire_action(:controller => 'pages', :action => 'menus')
# Expire a fragment
expire_fragment('all_available_products')
end
def expire_cache_for(food_menu)
# Expire the menus page now that we added a new FoodMenu
expire_action(:controller => 'pages', :action => 'menus')
# Expire a fragment
expire_fragment('all_available_food_menus')
end
end
I finally figured it out! I was able to get both fragment and action caching to work. From the server logs, it seems that Action Caching is a lot faster, so that's what I'm deploying with.
For Fragment Caching, I created a "food_menu_sweeper.rb" that observes both FoodMenu and Product and expires the fragment that I created in the partial, like so:
class FoodMenuSweeper < ActionController::Caching::Sweeper
observe FoodMenu, Product
def after_save(food_menu)
expire_cache(food_menu)
end
def after_destroy(food_menu)
expire_cache(food_menu)
end
def after_save(product)
expire_cache(product)
end
def after_destroy(product)
expire_cache(product)
end
private
def expire_cache(food_menu)
expire_fragment("menu items")
end
def expire_cache(product)
expire_fragment("menu items")
end
end
Here is the fragment in the partial:
<% cache("menu items") do %>
<% for food_menu in FoodMenu.full_list %>
...
<% end %>
<% end %>
FoodMenu.full_list is called within the fragment to cache the database query as well, which is defined in the food_menu.rb model:
def self.full_list
FoodMenu.includes(:products).all
end
Then, the key here is to place the cache_sweeper in "application_controller.rb"! This crucial hint came from reading this SO entry: Rails - fragment cache not expiring
cache_sweeper :food_menu_sweeper
This all works like a charm. When I refresh the page, the fragment is read from cache and no database calls are made. Once I update a product or a food_menu, the cache is cleared and the new data appears and is then cached.
For Action Caching, I added:
caches_action :menus
in my "pages_controller.rb", then removed the fragment cache from the partial, and replaced
expire_fragment("menu items")
in food_menu_sweeper.rb, with:
expire_action(:controller => '/pages', :action => 'menus')
The key here was the leading slash before "pages", which I found via this SO entry: rails caching: expire_action in another namespace
Without the leading slash, I was getting a "Can't convert symbol into integer" error when updating items via the ActiveAdmin interface.
Yay for determination, Google, and Stack Overflow!