I have a table CLIENTS with id, name and email fields and I am sending them emails using ActionMailer with 3rd party SMTP.
Now I want the clients to have subscription option too so I added "subscription" column with default value as true.
Now how to generate a link which can be put in views mailer template so when the user clicks on it, the subscription value changes to false so in future the client dont' get any email ? Do note that these clients are not my rails app users so I can't uses what is been suggested here Rails 3.2 ActionMailer handle unsubscribe link in emails
I found this link how to generate link for unsubscribing from email too which looked helpful but I thought may be in 3 years, we might have got a better solution
Here is my Complete Code -
#client.rb
attr_accessible :name, :company, :email
belongs_to :user
has_many :email_ids
has_many :emails, :through => :email_ids
before_create :add_unsubscribe_hash
private
def add_unsubscribe_hash
self.unsubscribe_hash = SecureRandom.hex
end
Here is Clients_controller.rb file
# clients_controller.rb
def new
#client = Client.new
respond_to do |format|
format.html
format.json { render json: #client }
format.js
end
end
def create
#client = current_user.clients.new(params[:client])
respond_to do |format|
if #client.save
#clients = current_user.clientss.all
format.html { redirect_to #client }
format.json { render json: #client }
format.js
else
#clients = current_user.clients.all
format.html { render action: "new" }
format.json { render json: #client.errors, status: :error }
format.js
end
end
end
def unsubscribe
#client = Client.find_by_unsubscribe_hash(params[:unsubscribe_hash])
#client.update_attribute(:subscription, false)
end
The code is working fine for existing records and the unsubscription is working perfectly, I am only having problem in creating new clients.
I have used #client in unsubscribe method as I am using this object in client_mailer.rb template (using #client or just using client, both are working!)
EDIT 2 -
_form.html.erb
<%= simple_form_for(#client, :html => {class: 'form-horizontal'}) do |f| %>
<%= f.input :name, :label => "Full Name" %>
<%= f.input :company %>
<%= f.input :email %>
<%= f.button :submit, class: 'btn btn-success' %>
<% end %>
I have copied the full track stack at http://jsfiddle.net/icyborg7/dadGS/
Try associating each client with a unique, but obscure, identifier which can be used to look up (and unsubscribe) the user via the unsubscribe link contained within the email.
Start by adding another column to your clients table called unsubscribe_hash:
# from command line
rails g migration AddUnsubscribeHashToClients unsubscribe_hash:string
Then, associate a random hash with each client:
# app/models/client.rb
before_create :add_unsubscribe_hash
private
def add_unsubscribe_hash
self.unsubscribe_hash = SecureRandom.hex
end
Create a controller action that will toggle the subscription boolean to true:
# app/controllers/clients_controller.rb
def unsubscribe
client = Client.find_by_unsubscribe_hash(params[:unsubscribe_hash])
client.update_attribute(:subscription, false)
end
Hook it up to a route:
# config/routes.rb
match 'clients/unsubscribe/:unsubscribe_hash' => 'clients#unsubscribe', :as => 'unsubscribe'
Then, when a client object is passed to ActionMailer, you'll have access to the unsubscribe_hash attribute, which you can pass to a link in the following manner:
# ActionMailer view
<%= link_to 'Unsubscribe Me!', unsubscribe_url(#user.unsubscribe_hash) %>
When the link is clicked, the unsubscribe action will be triggered. The client will be looked up via the passed in unsubscribe_hash and the subscription attribute will be turned to false.
UPDATE:
To add a value for the unsubscribe_hash attribute for existing clients:
# from Rails console
Client.all.each { |client| client.update_attribute(:unsubscribe_hash, SecureRandom.hex) }
Related
I'm trying a feature where user can request for offer and it can be accepted or rejected , I'm new to rails. i can't figure out what's the good way to proceed this.
offer create method
def create
#offer = Offer.new(offer_params)
pp offer_params
#barter = Barter.find(params[:barter_id])
#offer = Offer.new(offer_params)
#offer.barter = #barter
#offer.user = current_user
respond_to do |format|
if #offer.save
format.js do
#barter = Barter.find(params[:barter_id])
end
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: #review.errors, status: :unprocessable_entity }
end
end
end
offer submission
<%= form_for([ #barter, #barter.offers.new] ) do |form| %>
<%= form.text_area :message %><br>
<%= form.submit "leave" %>
<% end %>
here I want to make it accepted or rejected , I've given a boolean value and simply make it false when rejected
<%= form_tag([ #barter, #barter.offers.new] ) do %>
<%= hidden_field_tag :reject, :value => true %><br>
<%= submit_tag "reject" %>
<% end %>
is there a good way to do this? and how can i make it disappear when i accept this.
Sorry but thats not even close. You're just creating a new offer record in the form when what you should be doing is to update an existing record - and while you potentially do this through PATCH /offers/:id its going to be very ambigeuos in terms of intent.
The simplest way I cn think of handle this would be to simply add two additional RESTful routes to update the offers.
Start by adding the routes:
resources :offers, only: [] do
patch :accept
patch :decline
end
And en enum attribute to the model:
class AddStatusToOffers < ActiveRecord::Migration[7.0]
def change
add_column :offers, :status, :integer, default: 0, index: true
end
end
class Offer < ApplicationRecord
# ...
enum status: {
pending: 0,
accepted: 1,
rejected: 2
}
end
This is a better idea then adding a boolean since your boolean would either need to be a tri-state boolean (nullable) which is regarded as a very bad practice or default to false in which case you can't differentiate between the offers a users has replied to or not.
Then add the controller methods for your new endpoints:
class OffersController
before_action :set_coffer, only: %i{ show edit update destroy accept decline }
# ...
# PATCH /offers/:id/accept
# #TODO authorize that the user should actually be allowed the offer
def accept
if #offer.accepted!
redirect_to #offer, notice: 'Offer accepted'
else
redirect_to #offer, notice: 'Offer could not be accepted - please try again'
end
end
# PATCH /offers/:id/reject
# #TODO authorize that the user should actually be reject the offer
def reject
if #offer.rejected!
redirect_to #offer, notice: 'Offer rejected'
else
redirect_to #offer, notice: 'Offer could not be rejected - please try again'
end
end
private
def set_offer
#offer = Offer.find(params[:id])
end
end
You can then simply add buttons/links that send the request to update the offer:
<%= button_to "Accept", accept_offer_path(offer), method: :patch %>
<%= button_to "Reject", reject_offer_path(offer), method: :patch %>
This is not the only way to solve the issue. If you for example want to record a message where the user can say why they rejected an offer I would model replies to an offer as a completely seperate resource.
I have two models: one for contacts ("Contatos") and one for users ("Usuarios"). Contatos has_one Usuario , as follows:
class Contato < ApplicationRecord
has_one :usuario, dependent: :destroy
accepts_nested_attributes_for :usuario,
allow_destroy: true
And
class Usuario < ApplicationRecord
has_secure_password
belongs_to :contato
validates_presence_of :login, :password
validates_uniqueness_of :login
end
I want to use one form for creating and editing both models. The _form partial that I currently have is this:
<%= form_with(model: contato, local: true) do |contato_form| %>
<%= if contato.errors.any?
showferr contato
end %>
#Here are the inputs for contato, I cut them out so it wouldn't be too long to read.
Bellow (same file as above) there is a check box for the Contato model that I left on, it sets a Boolean in the model(and DB) telling if the contact has a user on not, additionally I use some JavaScript (Coffee) to toggle the whole user (Usuario) form part based on the checkboxe's value .
<div class="form-group">
<%= contato_form.label :possui_usuario, :class => 'inline-checkbox' do %>
Possui usuário
<%= contato_form.check_box :possui_usuario, {id: "hasUser", checked: #contato.possui_usuario} %>
<% end %>
</div>
</div>
<div id="userPart" class="findMe" <% unless #contato.possui_usuario %> style="display:none;" <% end %> >
<h2> Usuário: </h2>
<div class="container">
<%= contato_form.fields_for :usuario, #contato.usuario do |usuario_form| %>
<%= render partial: 'usuarios/campos_usuario', locals: {form: usuario_form, object: #contato} %>
<% end %>
</div>
</div>
<br/>
<div class="container-fluid text-right">
<%= contato_form.submit 'Confirmar', :class => 'btn-lg btn-success' %>
</div>
<% end %>
The partial form for the Usuario model is rendering ok, but what I want to do is to only create and/or validate the user part if the checkbox is selected (if I say that the contact does have a user).
Here's what I attempted last (there were many attempts):
At Contato model:
attr_accessor(:has_user)
#has_user = 0
before_validation do |record|
#has_user = record.possui_usuario
end
def self.user?
#has_user == 1
end
validates_presence_of :nome
validates_length_of :nome, in: 1..45
validates_presence_of :email
validates_format_of :email, with: email_regex
validates_associated :usuario, if: user?
Controller for Contato:
class ContatosController < ApplicationController
before_action :set_contato, only: [:show, :edit, :update, :destroy]
# GET /contatos
# GET /contatos.json
def index
#contatos = Contato.all
#page_title = 'Contatos'
end
# GET /contatos/1
# GET /contatos/1.json
def show
#page_title = 'Ver contato: ' + #contato.nome
end
# GET /contatos/new
def new
#contato = Contato.new
#contato.build_usuario
#contato.ativo = true
#page_title = 'Novo contato'
end
# GET /contatos/1/edit
def edit
#page_title = 'Editar contato: ' + #contato.nome
unless #contato.possui_usuario
#contato.build_usuario
end
end
# POST /contatos
# POST /contatos.json
def create
#contato = Contato.new(contato_params)
respond_to do |format|
if #contato.save
flash[:notice] = 'Contato foi criado com sucesso.'
format.html {redirect_to #contato}
format.json {render :show, status: :created, location: #contato}
else
flash[:warn] = "Erro ao criar contato."
format.html {render :new}
format.json {render json: #contato.errors, status: :unprocessable_entity}
end
end
end
# PATCH/PUT /contatos/1
# PATCH/PUT /contatos/1.json
def update
respond_to do |format|
if #contato.update(contato_params)
format.html {redirect_to #contato, notice: 'Contato foi atualizado com sucesso.'}
format.json {render :show, status: :ok, location: #contato}
else
format.html {render :edit}
format.json {render json: #contato.errors, status: :unprocessable_entity}
end
end
end
# DELETE /contatos/1
# DELETE /contatos/1.json
def destroy
#contato.destroy
respond_to do |format|
format.html {redirect_to contatos_url, notice: 'Contato deletado com sucesso.'}
format.json {head :no_content}
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_contato
#contato = Contato.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def contato_params
params.require(:contato).permit(:id, :empresa_id,
:ativo, :nome,
:cargo, :celular,
:email, :nascimento,
:observacoes, :mensagem_instantanea,
:tipo_msg_inst, :possui_usuario,
usuario_attributes: [:login, :password, :permissoes, :id, :contato_id, :_destroy])
end
end
Sorry for the long question and big code blocks.
I see two holes in the data presented currently ...
First, your controller action where create is called is where you should be testing to see if you are calling to the model / activerecord.
Something like ...
def create
if #contato && #contato.usuarios # might be able to just do last half
respond_to do |format|
if #contato = #contato.create!(contato_params) # note the bang or '!'
format.html { redirect_to #contato, notice: 'contato was successfully created.' }
else
format.html { render :new }
end
end
end
end
Without seeing your controller - I am going to guess you didn't nest your controller via Rails strong_param feature properly. Note here - these two won't run, I'm not quite sure what information is needed, but I wanted you to make sure if you are nesting your models and using a single controller - you are away you need to nest your models in strong_params (google search nested rails strong_params for thousands of help / hits).
params.require(:contato).permit(:login, :password, usuario: [id, ...] )
If that's not it - also tell us if all the functionality of create/read/update/destroy works normally & you are just looking to limit it to create in certain circumstances?
Update - based on the controller - just move your check for create from the model & move it to the controller at the start of the #create action ... maybe start with ...
def create
# Note - here you will have to inspect contato_params to find syntax
if contato_params[:usuario_attributes][:contato_id]
... rest of action wrapped in here ...
end
end
... once again ... you will need to work out exact syntax - but just like you did with the edit - this spot is where you control the creation - not in the model.
More specifically I see this #contato.possui_usuario in the form ... that's probably the variable you want to check against in your controller, but perhaps my suggestion is more important - I can't tell you that with certainty - I'm also not sure you need the has_user trick per say in model & might be tempted to do a controller version in the private method section ...
class ContatosController
private
def has_user?
... whatever ...
end
Clarification from comment:
If I move the control over the user form part to the controller (which
makes a lot of sense) how would I about canceling the
validates_associated part of the model in case the user decides that
this contact wont have any users?
You don't move the form control (defined as variable in the form), you move the model method that deals with the form control to the controller - then you can wrap it all in a transaction to rollback any other changes OR if you build your activerecord out with #build it will do it for you.
I have a very straight-forward task to fulfil --- just to be able to write comments under posts and if the comments fail validation display error messages on the page.
My comment model uses a gem called Acts_as_commentable_with_threading, which creates a comment model after I installed.
On my post page, the logic goes like this:
Posts#show => display post and a form to enter comments => after the comment is entered, redisplay the Post#show page which has the new comment if it passes validation, otherwise display the error messages above the form.
However with my current code I can't display error messages if the comment validation fails. I think it is because when I redisplay the page it builds a new comment so the old one was erased. But I don't know how to make it work.
My codes are like this:
Comment.rb:
class Comment < ActiveRecord::Base
include Humanizer
require_human_on :create
acts_as_nested_set :scope => [:commentable_id, :commentable_type]
validates :body, :presence => true
validates :first_name, :presence => true
validates :last_name, :presence => true
# NOTE: install the acts_as_votable plugin if you
# want user to vote on the quality of comments.
#acts_as_votable
belongs_to :commentable, :polymorphic => true
# NOTE: Comments belong to a user
belongs_to :user
# Helper class method that allows you to build a comment
# by passing a commentable object, a user (could be nil), and comment text
# example in readme
def self.build_from(obj, user_id, comment, first_name, last_name)
new \
:commentable => obj,
:body => comment,
:user_id => user_id,
:first_name => first_name,
:last_name => last_name
end
end
PostController.rb:
class PostsController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
def show
#post = Post.friendly.find(params[:id])
#new_comment = Comment.build_from(#post, nil, "", "", "")
end
end
CommentsController:
class CommentsController < ApplicationController
def create
#comment = build_comment(comment_params)
respond_to do |format|
if #comment.save
make_child_comment
format.html
format.json { redirect_to(:back, :notice => 'Comment was successfully added.')}
else
format.html
format.json { redirect_to(:back, :flash => {:error => #comment.errors}) }
end
end
end
private
def comment_params
params.require(:comment).permit(:user, :first_name, :last_name, :body, :commentable_id, :commentable_type, :comment_id,
:humanizer_answer, :humanizer_question_id)
end
def commentable_type
comment_params[:commentable_type]
end
def commentable_id
comment_params[:commentable_id]
end
def comment_id
comment_params[:comment_id]
end
def body
comment_params[:body]
end
def make_child_comment
return "" if comment_id.blank?
parent_comment = Comment.find comment_id
#comment.move_to_child_of(parent_comment)
end
def build_comment(comment_params)
if current_user.nil?
user_id = nil
first_name = comment_params[:first_name]
last_name = comment_params[:last_name]
else
user_id = current_user.id
first_name = current_user.first_name
last_name = current_user.last_name
end
commentable = commentable_type.constantize.find(commentable_id)
Comment.build_from(commentable, user_id, comment_params[:body],
first_name, last_name)
end
end
comments/form: (this is on the Posts#show page)
<%= form_for #new_comment do |f| %>
<% if #new_comment.errors.any? %>
<div id="errors">
<h2><%= pluralize(#new_comment.errors.count, "error") %> encountered, please check your input.</h2>
<ul>
<% #new_comment.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<% end %>
I would instead use nested routes to create a more restful and less tangled setup:
concerns :commentable do
resources :comments, only: [:create]
end
resources :posts, concerns: :commentable
This will give you a route POST /posts/1/comments to create a comment.
In your controller the first thing you want to do is figure out what the parent of the comment is:
class CommentsController < ApplicationController
before_action :set_commentable
private
def set_commentable
if params[:post_id]
#commentable = Post.find(params[:post_id])
end
end
end
This means that we no longer need to pass the commentable as form parameters. Its also eliminates this unsafe construct:
commentable = commentable_type.constantize.find(commentable_id)
Where a malicous user could potentially pass any class name as commentable_type and you would let them find it in the DB... Never trust user input to the point where you use it to execute any kind of code!
With that we can start building our create action:
class CommentsController < ApplicationController
before_action :set_commentable
def create
#comment = #commentable.comments.new(comment_params) do |comment|
if current_user
comment.user = current_user
comment.first_name = current_user.first_name
comment.last_name = current_user.last_name
end
end
if #comment.save
respond_to do |format|
format.json { head :created, location: #comment }
format.html { redirect_to #commentable, success: 'Comment created' }
end
else
respond_to do |format|
format.html { render :new }
format.json { render json: #comment.errors, status: 422 }
end
end
end
private
# ...
def comment_params
params.require(:comment).permit(:first_name, :last_name, :body, :humanizer_answer, :humanizer_question_id)
end
end
In Rails when the user submits a form you do not redirect the user back to the form - instead you re-render the form and send it as a response.
While you could have your CommentsController render the show view of whatever the commentable is it will be quite brittle and may not even provide a good user experience since the user will see the top of the post they where commenting. Instead we would render app/views/comments/new.html.erb which should just contain the form.
Also pay attention to how we are responding. You should generally avoid using redirect_to :back since it relies on the client sending the HTTP_REFERRER header with the request. Many clients do not send this!
Instead use redirect_to #commentable or whatever resource you are creating.
In your original code you have totally mixed up JSON and HTML responses.
When responding with JSON you do not redirect or send flash messages.
If a JSON POST request is successful you would either:
Respond with HTTP 201 - CREATED and a location header which contains the url to the newly created resource. This is preferred when using SPA's like Ember or Angular.
Respond with HTTP 200 - OK and the resource as JSON in the response body. This is often done in legacy API's.
If it fails do to validations you should respond with 422 - Unprocessable Entity - usually the errors are rendered as JSON in the response body as well.
Added.
You can scrap your Comment.build_from method as well which does you no good at all and is very idiosyncratic Ruby.
class PostsController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
def show
#post = Post.friendly.find(params[:id])
#new_comment = #post.comments.new
end
end
Don't use line contiuation (\) syntax like that - use parens.
Don't:
new \
:commentable => obj,
:body => comment,
:user_id => user_id,
:first_name => first_name,
:last_name => last_name
Do:
new(
foo: a,
bar: b
)
Added 2
When using form_for with nested resources you pass it like this:
<%= form_for([commentable, comment]) do |f| %>
<% end %>
This will create the correct url for the action attribute and bind the form to the comment object. This uses locals to make it resuable so you would render the partial like so:
I'm assuming your form_for submits a POST request which triggers the HTML format in CommentsController#create:
def create
#comment = build_comment(comment_params)
respond_to do |format|
if #comment.save
make_child_comment
format.html
format.json { redirect_to(:back, :notice => 'Comment was successfully added.')}
else
format.html
format.json { redirect_to(:back, :flash => {:error => #comment.errors}) }
end
end
end
So, if #comment.save fails, and this is an HTML request, the #create method renders create.html. I think you want to render Posts#show instead.
Keep in mind that if validations fail on an object (Either by calling save/create, or validate/valid?), the #comment object will be populated with errors. In other words calling #comment.errors returns the relevant errors if validation fails. This is how your form is able to display the errors in #new_comment.errors.
For consistency, you'll need to rename #new_comment as #comment in the posts#show action, otherwise you'll get a NoMethodError on Nil::NilClass.
TL;DR: You're not rendering your form again with your failed #comment object if creation of that comment fails. Rename to #comment in posts, and render controller: :posts, action: :show if #comment.save fails from CommentsController#create
I have figured out the answer myself with the help of others here.
The reason is that I messed up with the JSON format and html format (typical noobie error)
To be able to display the errors using the code I need to change two places ( and change #comment to #new_comment as per #Anthony's advice).
1.
routes.rb:
resources :comments, defaults: { format: 'html' } # I set it as 'json' before
2.
CommentsController.rb:
def create
#new_comment = build_comment(comment_params)
respond_to do |format|
if #new_comment.save
make_child_comment
format.html { redirect_to(:back, :notice => 'Comment was successfully added.') }
else
commentable = commentable_type.constantize.find(commentable_id)
format.html { render template: 'posts/show', locals: {:#post => commentable} }
format.json { render json: #new_comment.errors }
end
end
end
I have really been scratching my head on this and would greatly appreciate help. I have a store setup where people can take courses. I have a course model, order model, and coupon model. Here are the associations in the models
class Course < ActiveRecord::Base
belongs_to :category
has_many :orders
has_many :coupons
end
class Order < ActiveRecord::Base
belongs_to :course
belongs_to :user
belongs_to :coupon
end
class Coupon < ActiveRecord::Base
belongs_to :course
has_many :orders
end
I have a very simple coupon model setup that has code and newprice columns. I want the ability for someone to be able to fill out the coupon form on the new order page and it to update the price.
In my my view for new order I have two forms one for the new order and one for the coupon. How do check in my controller if a user has entered the correct coupon code? How do I update the coupon price to be shown instead of the course price?
here is my order controller
class OrdersController < ApplicationController
before_action :set_order, only: [:show, :edit, :update, :destroy]
before_action :authenticate_user!
def index
#orders = Order.all
end
def show
end
def new
course = Course.find(params[:course_id])
#course = Course.find(params[:course_id])
#order = course.orders.build
#coupon = Coupon.new
#user = current_user.id
#useremail = current_user.email
end
def discount
course = Course.find(params[:course_id])
#order = course.orders.build
#user = current_user.id
#useremail = current_user.email
end
def edit
end
def create
#order = current_user.orders.build(order_params)
if current_user.stripe_customer_id.present?
if #order.pay_with_current_card
redirect_to #order.course, notice: 'You have successfully purchased the course'
else
render action: 'new'
end
else
if #order.save_with_payment
redirect_to #order.course, notice: 'You have successfully purchased the course'
else
render action: 'new'
end
end
end
def update
if #order.update(order_params)
redirect_to #order, notice: 'Order was successfully updated.'
else
render action: 'edit'
end
end
def destroy
#order.destroy
redirect_to orders_url
end
private
# Use callbacks to share common setup or constraints between actions.
def set_order
#order = Order.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def order_params
params.require(:order).permit(:course_id, :user_id, :stripe_card_token, :email)
end
end
You can accomplish this with an AJAX request using the form_for helper with the :remote option.
Summary
Set :remote option to true for your coupons form to submit the AJAX request.
Create controller action to handle the AJAX request from the form.
Use JavaScript to respond to the controller action to update your orders form (the other form in your view) with the new price information, etc.
AJAX request using `:remote`
Here's some example code representing your coupon form :
<%= form_for #coupon, method: :post, url: check_coupon_code_path, remote: true do |f| %>
<%= f.text_field :coupon_code, :placeholder => "Enter your coupon" %>
<%= f.submit "Submit Coupon Code" %>
<% end %>
Notice the following:
The :remote option for the form_for tag is set to true.
The :url option is the path to your controller action in your CouponsController. Because the :remote option is set to true, the request will be posted to this :url option as an AJAX request.
In this code example, it's assuming it has a route defined like this in the routes.rb file to handle the AJAX request for checking the coupon code:
post 'check_coupon_code' => 'coupons#check_coupon_code'
Note: In the forms_for helper, the :url option appends _path to the prefix defined in the routes.rb file.
Bonus note: Use the command rake routes to see the available routes and their respective controller action targets.
Handle AJAX request in the Controller
In your CouponsController, define the action check_coupon_code to handle your AJAX request from the above form_for:
def check_coupon_code
# logic to check for coupon code here
respond_to do |format|
if # coupon code is valid
format.js {}
else
# some error here
end
end
end
Notice the format.js in the respond_to block of the action. This allows the controller to respond to the AJAX request with JavaScript to update your orders form in your view. You'll have to define a corresponding app/views/coupons/check_coupon_code.js.erb view file that generates the actual JavaScript code that will be sent and executed on the client side (or name the JavaScript file check_coupon_code.js.coffee if you're using CoffeeScript).
Updating with JavaScript
The JavaScript in your check_coupon_code.js.erb file will then update the price in your order form.
WARNING: Even if you use JavaScript to change the order price on the client-side (i.e. the browser), it is critical to validate the actual price again in the back-end (i.e. in your controller) in case some malicious user tries to manipulate the browser's request, etc.
You can see the official RailsGuide for another example.
I have an Order model.
Customers get a handful of consumer-friendly views that let them create and view their own orders, all backed by an Orders controller.
Admins get the full range of views to create, edit, view, delete and manage orders, backed by a Purchases controller.
As far as I can tell, the Purchases controller should only be speaking to the Order model, but the following error message makes me think it's looking for a non-existant Purchase model:
ActiveRecord::StatementInvalid in PurchasesController#new
NameError in PurchasesController#new
uninitialized constant Purchase
Rails.root: /Users/steven/Dropbox/testivate
Is that what the error means? If it does, how do I stop the Purchases controller from trying to find a Purchase model?
My code...
app/controllers/purchases_controller.rb:
class PurchasesController < ApplicationController
def new
#purchase = Order.new
respond_to do |format|
format.html # new.html.erb
format.json { render json: #purchase }
end
end
def create
#purchase = Order.new(params[:purchase])
respond_to do |format|
if #purchase.save
format.html { redirect_to purchase_path(#purchase), notice: 'Purchase was successfully created.' }
format.json { render json: #purchase, status: :created, location: #purchase }
else
format.html { render action: "new" }
format.json { render json: #purchase.errors, status: :unprocessable_entity }
end
end
end
end
/config/routes.rb:
Testivate::Application.routes.draw do
resources :orders
resources :purchases
end
/app/views/purchases/new.html.haml:
.textbox
%p#notice= notice
%h1 New Purchase
= render 'form'
= link_to 'List Purchases', purchases_path
/app/views/purchases/_form.html.haml:
= simple_form_for #purchase do |f|
= f.error_notification
= f.input :name
= f.button :submit
Update: so I've just realised that 'transaction' is a reserved word in Rails so I've changed that. But is there anything else I need to fix?
*Update 2: When I completely comment out the #new view and the _form, I still get the error, so I think the problem is in my controller or routes or somewhere other than with my use of simple_form.*
Update:
This is half of the answer. For the other half see the comments on the question.
Original answer:
Part of the problem here is that by default form_for (which simple_form_for is built atop) assumes certain things about what paths to use for a given record, which it derives from the class of that record. In your case, since #purchase is actually an instance of the Order class, those assumptions are going to screw things up for you.
If you look at the form generated by simple_form_for, you'll see that it gives the form both an id and a class of new_order, and that the fields in the form have names like order[name]. To get rails to use purchase instead of order in the form, you have to pass it an :as option.
From the docs for form_for:
If you have an object that needs to be represented as a different parameter, like a Person that acts as a Client:
And the example they give:
<%= form_for(#person, :as => :client) do |f| %>
...
<% end %>
In your case, you'd actually want to tell it to treat #purchase like a "purchase":
= simple_form_for #purchase, :as => :purchase do |f|
= f.error_notification
= f.input :name
= f.button :submit
That will get the form ids, etc. right, so that you can use e.g. params[:purchase] in your create action as you have above. But this is not enough, because the action (URL) for the form will still end up being /orders. To change that, add an :url option:
= simple_form_for #purchase, :as => :purchase, :url => purchases_path(#purchase) do |f|
= f.error_notification
= f.input :name
= f.button :submit
This will also solve the other question you posted.