If I want to decrement the counter attribute of an object, is it better to add it to the update action, or the destroy action, or is it better to create a new action?
To explain better, let's say you have a Product and a CartItem class, and every time a user adds the same product to her cart, instead of having two copies of the same CartItem object, you increment the amount attribute of the CartItem.
If I want to let users decrement the amount, what should I use? For example, is something like the code below a viable and conventional way of doing this? Or is it better to create a new action?
View:
...
<%= button_to 'Remove One', [cart_item, remove_one: true], method: :patch %>
...
Controller:
def update
if params[:remove_one]
if #cart_item.quantity > 1
#cart_item.decrement!(:quantity)
flash[:notice] = "Removed one of #{#cart_item.product.title}"
redirect_to :back
else
destroy
end
end
return
end
Note: I understand that the title sounds a bit misleading, but I am not looking for a way to decrement an attribute in Rails. What I want to know is, if you have such a requirement, which action do you put that in? How do you decide what goes in which action?
Edit: Guys I really expected something credible like an example from a well-maintained source-code, a high-rated book, etc. Under these circumstances, unfortunately I won't be able to award the bounty to anyone.
I don't think there's a 100% correct answer but from my experience I see two options that are better than your code example:
Have a decrement action on the controller and in the controller you use the decrement_counter method (doc). This helps if you have concurrent updates on the same model (though that's not usually the case for shopping carts).
Have an update method and in the view render a small form:
The view
<%= form_for cart_item do |form| %>
<%= form.hidden_field :quantity, cart_item.quantity-1 %>
<%= form.submit 'Remove One'
<% end %>
In the controller
def update
#cart_item.update(cart_item_params)
#cart_item.destroy if #cart_item.quantity == 0
#in the above
redirect_to :back
end
def cart_item_params
params.require(:cart_item).permit(:quantity)
end
add a dedicated method in CartItem class...such as
##it will return either true or false
def !!update_amount
###use if condition if you need
if self.quantity > 1
self.update_attribute :amount,self.amount+=amount-1
end
end
use it anywhere you want such as:--
def update
if params[:remove_one]
##call the method to get only true or false
if #cart_item.update_amount
flash[:notice] = "Removed one of #{#cart_item.product.title}"
redirect_to :back
else
destroy
end
end
return
end
To me it make sense semantically to have this in a separate method.
You are updating an attribute on a model, which means that you should use the PATCH method, and preferably invoke the update method per rails' conventions, so in my opinon this separate method invocation should be performed without altering the RESTful routes.
before_action :inventory_check, only: :update
def update
if #cart_item.update
flash[:notice] = "Updated your cart."
else
flash[:error] = "Couldn't update your cart."
end
redirect_to :back
end
def destroy
#cart_item.destroy
flash[:notice] = "Item removed from your cart."
redirect_to :back
end
private
def inventory_check
if params[:decrement] && #cart_item.quantity == 1
redirect_to :destroy
end
end
Seems to me that you CartItem object describes the many-to-many relationship between Product and Card. So when you are adding a Product to a Cart, you are just updating (or creating if there isn't one yet) the content of that specific Production-Cart relationship.
In this sense, I would suggest you to just put the decrement in the update method. However, if you want to do some more logic to determine whether this relationship exists or not, you can also create a new method and do all the validations in it.
Related
I am working with a Rails application that allows users to create projects. Inside these projects, users can make lists. I am trying to figure out how to allow users to choose their "Default Working Project" from the projects index page. This would then propagate throughout the app, showing only lists associated with the current project. What is the best approach to making something like this.
You can achieve this easily by adding default_working_project_id field to your users table.
Then in your controller index set:
#default_working_project = current_user.default_working_project
In your user model add:
belongs_to :default_working_project, class_name: Project, foreign_key: :default_working_project_id
You can create your own action. In your routes file:
resources :projects do
member do
get 'set_default'
end
end
In your projects_controller:
def set_default
project.find params[:id]
current_user.default_working_project_id = project.id
respond_to do |format|
if current_user.save
format.html { redirect_to projects_path }
else
format.html { render 'index', notice: "your error message" }
end
end
end
In your views just add
link_to 'set default', set_default_project_path(project.id)
UPDATED
To remove current default project id from user:
You can make some methods to achieve this, like:
In your Project model
If you have a relation that project belongs_to user try this.
def is_a_current_project?
self.id == self.user.default_working_project_id
end
Then create an after_destroy :remove_current_project_relation callback method.
And the method, I recommend to add it inside your private methods:
def remove_current_project_relation
if is_a_current_project?
self.user.default_working_project = nil
end
end
Create a database field in Users table with default_project_id and set it.
On the model set:
def default_project
return projects.find_by_id(default_project_id) if default_project_id
false
end
And then, you can use something like this:
lists = user.default_project ? user.default_project.lists : user.lists
If only one user can see his projects and other users won't be able to see other user's projects, my suggestion is:
Make a boolean value is_default in the projects table. Add it with a migration.
Add :is_default to def project_params in the controller.
In the projects index page use:
<%= render #projects %>
Create file _project.html.erb in views/projects folder, add to it:
<%= form_for project, remote: true do |f| %>
# some project data
<%= f.check_box :is_default, class: 'project_default' %>
<% end %>
In projects_controller:
def update
#project = Project.find(params[:id])
if #project.update_attributes(project_params)
respond_to do |format|
format.html { redirect_to project_page } # this will run when you update project from edit page in form without 'remote: true'
format.js # this will run if you update project with 'remote: true' form
end
end
end
In projects.coffee in assets/javascripts folder:
$('input.project_default').change -> $(this).closest('form').submit()
Create update.js.erb in the views/projects folder, add to it:
$('#edit_project_<%= #project.id %>').replaceWith("<%= j render 'project' %>");
In projects_helper
def current_project
current_user.projects.find_by(is_default: true)
end
Maybe you'll need to change these a little, based on your tasks. This solution will update projects through JavaScript.
Also it would be great to add a method in the Project model, which will make the previous default project not default when the user makes other project default and so on.
When you need to use lists from default project you can use default_project.lists in your views.
I'm trying to increment the value of a column in my model when a link is clicked and i keep getting an undefined method error on the attribute and i have no idea what i'm doing wrong
model
class VisitorsController < ApplicationController
def inc_adviser
self.adviser = self.adviser + 1
self.save
redirect_to root_path
end
end
route
match '/adviser' => 'visitors#inc_adviser'
view
<%= link_to 'Adviser', '/adviser', method: :post %>
Can anyone suggest what the problem is?
Thanks
self in that context is the controller instance, not the model instance you seem to expect it to be. You need to get a model instance from somewhere and increment the counter on that:
def inc_adviser
# Something like this, don't have enough information to be more specific.
model = WhateverModel.find(params[:id])
model.adviser = model.adviser + 1
model.save
redirect_to root_path
end
That is, of course, subject to race conditions so you should use increment_counter instead:
def inc_adviser
WhateverModel.increment_counter(:adviser, params[:id])
redirect_to root_path
end
So this has been asked previously, but with no satisfying answers.
Consider two models, User, and Subscription associated as such:
class User < ActiveRecord::Base
has_one :subscription, dependent: :destroy
end
class Subscription < ActiveRecord::Base
belongs_to :user
end
Inside of SubscriptionsController, I have a new action that looks like this
def new
user = User.find(params[:user_id])
#subscription = user.build_subscription
end
Given that a subscription already exists for a user record, I'm faced with the following problem:
user.build_subscription is destructive, meaning that simply visiting the new action actually destroys the association, thereby losing the current subscription record.
Now, I could simply check for the subscription's existence and redirect like this:
def new
user = User.find(params[:user_id])
if user.subscription.present?
redirect_to root_path
else
#subscription = user.build_subscription
end
end
But that doesn't seem all that elegant.
Here's my question
Shouldn't just building a tentative record for an association not be destructive?
Doesn't that violate RESTful routing, since new is accessed with a GET request, which should not modify the record?
Or perhaps I'm doing something wrong. Should I be building the record differently? Maybe via Subscription.new(user_id: user.id)? Doesn't seem to make much sense.
Would much appreciate an explanation as to why this is implemented this way and how you'd go about dealing with this.
Thanks!
It depends on what you want to do
Thoughts
From what you've posted, it seems the RESTful structure is still valid for you. You're calling the new action on the subscriptions controller, which, by definition, means you're making a new subscription (not loading a current subscription)?
You have to remember that Rails is basically just a group of Ruby classes, with instance methods. This means that you don't need to keep entirely to the RESTful structure if it doesn't suit
I think your issue is how you're handling the request / action:
def new
user = User.find(params[:user_id])
#subscription = user.build_subscription
end
#subscription is building a new ActiveRecord object, but doesn't need to be that way. You presumably want to change the subscription (if they have one), or create an association if they don't
Logic
Perhaps you could include some logic in an instance method:
#app/models/user.rb
Class User < ActiveRecord::Base
def build
if subscription
subscription
else
build_subscription
end
end
end
#app/controllers/subscriptions_controller.rb
def new
user = User.find(params[:user_id])
#subscription = user.build
end
This will give you a populated ActiveRecord, either with data from the subscription, or the new ActiveRecord object.
View
In the view, you can then use a select box like this:
#app/views/subscriptions/new.html.erb
<%= form_for #subscription do |f| %>
<%= "User #{params[:user_id]}'s subscription: %>
<%= f.collection_select :subscription_id, Subscription.all,:id , :name %>
<% end %>
They are my thoughts, but I think you want to do something else with your code. If you give me some comments on this answer, we can fix it accordingly!
I also always thought, that a user.build_foobar would only be written to the db, if afterwards a user.save is called. One question: After calling user.build_subscription, is the old subscription still in the database?
What is the output user.persisted? and user.subscription.persisted?, after calling user.build_subscription?
Your method to check if a subscription is present, is IMHO absolutely ok and valid.
I came across this today and agree that deleting something from the db when you call build is a very unexpected outcome (caused us to have bad data). As you suggested, you can work around if very easily by simply doing Subscription.new(user: user). I personally don't think that is much less readable then user.build_subscription.
As of 2018 Richard Peck's solution worked for me:
#app/models/user.rb
Class User < ActiveRecord::Base
def build_a_subscription
if subscription
subscription
else
build_subscription
end
end
end
My issue was that a user controller didn't have a new method, because users came from an api or from a seed file.
So mine looked like:
#app/controllers/subscriptions_controller.rb
def update
#user = User.find(params[:id])
#user.build_a_subscription
if #user.update_attributes(user_params)
redirect_to edit_user_path(#user), notice: 'User was successfully updated.'
else
render :edit
end
end
And I was finally able to have the correct singular version of subscriptions in my fields_for, so :subscription verses :subscriptions
#app/views
<%= f.fields_for :subscription do |sub| %>
<%= render 'subscription', f: sub %>
<% end %>
Before I could only get the fields_for to show in the view if I made subscriptions plural. And then it wouldn't save.
But now, everything works.
I'm developing a way for a user to gradually complete their profile, a bit like LinkedIn and others do. Depending on where they are in completing their information they'll be asked one of the following:
Sign up to a run
Add a goal
Add an avatar
Thanked for completing their profile
Having given it some thought, I think I'll approach this using the state_machine gem.
I've got as far as adding the following to the user model:
state_machine :profile_state, :initial => :needs_to_sign_up_to_a_run, :namespace => 'profile' do
event :signed_up_for_run do
transition :needs_to_sign_up_to_a_run => :needs_goal
end
event :completed_goal do
transition :needs_goal => :needs_avatar
end
event :provided_avatar do
transition :needs_avatar => :complete
end
state :needs_to_sign_up_to_a_run
state :needs_goal
state :needs_avatar
state :complete
end
However, I'm not sure that this is the best way to define the events or transitions.
Given the user can complete the goal/avatar/signup in multiple ways (i.e. from their user edit page, from a right hand panel, as part of the signup flow). So, in practice they could provide an avatar prior to a goal, and therefore the completed_goal isn't right.
Perhaps I could use some sort of validation to determine the state?
Maybe a state machine is completely the wrong approach.
I'd love any advice on approaching this design problem.
I would say state_machine is not good choice here.
I would recommend wicked, Step-by-step wizard controller.
here is a screen cast on it
Going on the above, I decided to use Wicked. I implemented it as follows:
class CallsToActionController < Wicked::WizardController
layout false
steps :sign_up_to_run, :add_goal, :add_avatar, :thank
def show
#user = current_user
case step
when :sign_up_to_run
skip_step if #user.attendances.intended.size > 0
when :add_goal
skip_step if #user.goals.count > 0
when :add_avatar
skip_step unless #user.avatar.file.nil?
end
render_wizard
end
def update
#user = current_user
render_wizard
end
end
This allowed me to have a series of partials in app/views/calls_to_action such as add_goal:
<div id="goalForm">
<% #goal = current_user.goals.new %>
<%= render :partial => 'goals/form' %>
</div>
I then handled submission through a standard controller:
class GoalsController < ApplicationController
def create
#goal = current_user.goals.new(params[:goal])
respond_to do |format|
if #goal.save
format.js
else
format.js { render 'reload' }
end
end
end
end
To tie it all together, as I wanted the wizard to sit in a div within the page, the javascript create action looked like this:
$("#call-to-action-response").html("<%= escape_javascript(render :partial => 'goals/success') %>");
$("#call-to-action").load("<%= pre_member_calls_to_action_url(:sign_up_to_run) %>").hide().fadeIn('slow');
Hopefully that's useful for someone attempting something similar!
I am trying to figure out the best way to do the following (there are a few ways I can think of, but I want to know what the best way to handle it is):
A user is putting together a shipment, and then clicks the "Send" link, which sends him to the /shipments/:id/confirm page. The confirm action checks to see if the user has a completed ShippingAddress; if not, it sends him to the ShippingAddress#new. (If he does, it render the confirm page.
I want the user to be able to complete the ShippingAddress#new page, submit it, and then be redirect back to the /shipments/:id/confirm. How can I do that? How can I pass the :id to the ShippingAddress#new page without doing something like redirect_to new_shipping_address_path(shipment_id: #shipment.id) in the Shipment#confirm action? Or is that the best way to do that?
class ShipmentsController < ApplicationController
def confirm
#shipment = Shipment.where(id: params[:id]).first
unless current_user.has_a_shipping_address?
# Trying to avoid having a query string, but right now would do the below:
# in reality, there's a bit more logic in my controller, handling the cases
# where i should redirect to the CardProfiles instead, or where I don't pass the
# shipment_id, and instead use the default shipment.
redirect_to new_shipping_address_path(shipment_id: #shipment.id)
end
end
end
class ShippingAddressesController < ApplicationController
def new
#shipment = Shipment.where(id: params[:shipment_id]).first
end
def create
#shipment = Shipment.where(id: params[:shipment_id]).first
redirect_to confirm_shipment_path(#shipment)
end
end
[In reality, there is also a CardProfiles#new page that needs to be filled out after the shipping address is].
Try calling render instead of redirect_to, and set the id into an instance variable. Adjust the view logic to pull that instance variable if it exists.
#shipment_id = #shipment.id
render new_shipping_address_path
In the view
<%= form_for #shipment_address do |f| %>
<% if #shipment_id %>
<%= hidden_field_tag :shipment_id, #shipment_id %>
<% end %>
I don't know your view logic entirely, but giving an example.