Updating A Single Attribute in Rails from email - ruby-on-rails

i'm having some issues trying to update an attribute outisde my web app (No route matches [GET] "/admin/justifications/19/approve").
The user should approve or reject a permission from their emails...
admin/justifications_controller.rb
class JustificationsController < BaseController
before_action :find_justification
# PATCH/PUT /admin/justifications/1/approve
def approve
#justification.approve
#justification.create_activity :approve, owner: current_user, recipient: #justification.user
redirect_to request.referer
end
# PATCH/PUT /admin/justifications/1/reject
def reject
#justification.reject
#justification.create_activity :reject, owner: current_user, recipient: #justification.user
redirect_to request.referer
end
routes
scope :admin, module: :admin do
resources :justifications, except: :all do
member do
patch :approve
patch :reject
end
end
...
end
Tho this works well in my web page, but it breaks when users try to open the generated links sent to their emails.
Is it something im missing here??
Any help would be great. Thnks!!

Your approve action is only available via PATCH or PUT and the link you press in your email sends the request via GET
There are lots of questions in SO asking how to send a different method than GET from the link you send in the email and the answer for that is: It is not possible. You have to open an GET action to be accessed from your email links.

There is no way to natively add links which send a PATCH request - Rails uses a data-method attribute together with some clever javascript to fake PATCH, PUT and DELETE requests.
However this only works if jquery-ujs is loaded in the client, which is problematic since many email clients and even webmail clients block emails from running javascript for security reasons.
What you need to do is add a GET route.

Related

No route matches [GET] when trying to [PATCH] with link_to

I want to send clients that did not complete a checkout an email with a magic link that will log them in before hitting an update action in a controller.
I'm sending the following link in the email body:
<%= link_to(
"Continue to checkout",
"#{checkout_url(host: #account.complete_url, id: #user.current_subscription_cart)}?msgver=#{#user.create_message_verifier}",
method: :patch,
subscription_cart: { item_id: #item_id },
) %>
My checkouts_controller has an update action:
def update
# update cart with item_id param and continue
end
And my routes look like this:
resources :checkouts, only: [:create, :update]
which gives the following update route:
checkout_path PATCH /checkouts/:id(.:format) checkouts#update
The link_to in the email body produces a link with a data-method="patch" property
<a data-method="patch" href="https://demo.test.io/checkouts/67?msgver=TOKEN">Continue to checkout</a>
=> https://demo.test.io/checkouts/67?msgver=TOKEN
but when I click on it I get the following error:
No route matches [GET] "/checkouts/67"
Why is it attempting a GET request when I'm specifying method: :patch ?
As pointed out by #AbM you need to send out a link to a route that responds to GET requests. Emails clients are unlikely to let you run JS or include forms in the email body - so you shouldn't assume that you'll be able to send anything but GET.
If you want an example of how this can be done you don't have to look further then the Devise::Confirmable module that solves pretty much the exact same problem:
Prefix Verb Method URI Pattern Description
new_user_confirmation GET /users/confirmation/new Form for resending the confirmation email
user_confirmation GET /users/confirmation The link thats mailed out with a token added - actually confirms the user
POST /users/confirmation Resends the confirmation email
The beauty of this design is that users confirmation are modeled as a RESTful resource even if no separate model exists.
In your case the implementation could look something like:
resources :checkouts do
resource :confirmation, only: [:new, :create, :show]
end
# Handles email confirmations of checkouts
class ConfirmationsController < ApplicationController
before_action :set_checkout
# GET /checkouts/1/confirmation/new
# Form for resending the confirmation email
def new
end
# POST /checkouts/1/confirmation
# Resends the confirmation email - because shit happens.
def create
#todo generate new token and resend the confirmation email
end
# GET /checkouts/1/confirmation&token=jdW0rSaYXWI7Ck_rOeSL-A
# Confirms the checkout by verifying that a valid token is passed
def show
if #checkout.valid_token?(params[:token])
#checkout.confirm!
redirect_to '/whatever_the_next_step_is'
else
flash.now('Invalid token')
render :new, status: :unauthorized
end
end
private
def set_checkout
#checkout = Checkout.find(params[:checkout_id])
end
end
Here we are taking a slight liberty with the rule that that GET requests should always be idempotent as clicking the link actually updates the checkout. Its a fair tradeoff though as it requires one less click from the user.
If you want to maintain that idempotency you could send a link to the edit action which contains a form to update the confirmation. Devise::Invitable does this.

with Rspec destroy is missing a template for this request format and variant

I have a Ruby on Rails application with a chat. the user creates a Chat and ActionCable updates the page on the user side. If the user wants to delete the Chat, ActionCable deletes the Chat on the user side. everything works perfectly. but if I write the test for that the test fails.
this is my controller.
def destroy
chat = Chat.find(params[:id])
chat.comments.delete_all
chat.likes.delete_all
id = '#chat-to-delete-id'+chat.id.to_s
chat.delete
ActionCable.server.broadcast 'room_channel',
delete: id
end
This is my spec
it 'delete a chat' do
sign_in(user)
post :create, params: {chat:{body: 'hello'}}
get :destroy, params: {id: Chat.last.id}
expect(response.status).to eq (200)
end
and this is the error I get when I run the test.
ActionController::UnknownFormat: PublicationsController#destroy is
missing a template for this request format and variant
request.formats: ["text/html"]
request.variant: []
NOTE! For XHR/Ajax or API requests, this action would normally respond with 204 No Content: an empty white screen. Since you're loading it in a web browser, we assume that you expected to actually render a template, not nothing, so we're showing an error to be extra-clear. If you expect 204 No Content, carry on. That's what you'll get from an XHR or API request. Give it a shot.
I don't need to render or redirect something because I update the page with ActionCable.
if I do redirect_to something, the test is ok but it reloads the page.
Any help or suggestion to pass that test
If you're using the resources :chat route helper, then the destroy route by default doesn't respond to GET requests. If you run rails routes you'll see something like this
chats GET /chats(.:format) chats#index
POST /chats(.:format) chats#create
new_chat GET /chats/new(.:format) chats#new
edit_chat GET /chats/:id/edit(.:format) chats#edit
chat GET /chats/:id(.:format) chats#show
PATCH /chats/:id(.:format) chats#update
PUT /chats/:id(.:format) chats#update
DELETE /chats/:id(.:format) chats#destroy
So, what you're actually requesting with get :destroy, params: {id: Chat.last.id} is the show action
try delete :destroy, params: {id: Chat.last.id} instead

Sending Format with Rspec GET

I'm attempted to test the mobile version of my rails site, but i can't seem to get the following code to work:
let(:uri) { '/' }
it 'routes to #mobile_index' do
get uri, :format => 'mobile'
controller.response.should route_to('home#index_mobile')
end
What's the proper way to send this sort of request so its seen by the app as coming from a mobile source? I've looked up a lot about setting the user agent, but i can't get any of those to work either. I'm using Rspec version 2.14.2.
How do you check if whether to redirect to mobile page or to the normal?
For this testcode to work you must be having something like this in your application#index
respond_to do |format|
format.mobile do
# redirect to mobile
end
format.html
end
This means if you call '/index' (or '/' ) and if you call '/index.mobile' it would be
redirecting to the mobile page
Because you've written something about the User Agent i guess this is your criterium for
distinguishing between mobile and normal version.
HTTP Headers in rails tests are set by the request.env method. Their names are prefixed
with HTTP_, capitalized and have dashes replaced by underscores.
so for setting the User-Agent header you just do
request.env['HTTP_USER_AGENT'] = "WWW-Mechanize"
and then perform the get call.
If you are checking only one and not multiple controllers in integration i would also make this a functional test of the Application Controller (or whatever controller responsible for the home action)
describe ApplicationController do
describe "GET index" do
it "redirects mobile agents to the mobile version" do
request.env['HTTP_USER_AGENT'] = 'Ipod ...'
#calls "/index" unless different routing configured
get :index
expect(response).to redirect_to <index_mobile_path> #path helper depends on route config
end
end
end

How to redirect from /:id to /:friendly_id

Is is possible to force a 301 redirect when someone attempts to browse to a page using the old /:id URL, rather than than the preferred /:friendly_id link?
Apparently such redirections help to tell Google that you have updated the link.. so it stops displaying the old non-friendly link.
With the latest version of friendly_id (5.0.3 at the time of writing this answer) and Rails 4, I do this in the controller:
class ItemsController < ApplicationController
before_action :set_item, only: [:show, :edit, :update, :destroy]
...
private
def set_item
#item = Item.friendly.find(params[:id])
redirect_to action: action_name, id: #item.friendly_id, status: 301 unless #item.friendly_id == params[:id]
end
end
Here's a description of the redirect_to line, broken down piece by piece:
action: action_name retains the action that you're connecting to (which can be show, edit, update, or destroy based on the before_action that's in place) so that if you're accessing /items/1/edit you will be redirected to /items/pretty-url/edit
id: #item.friendly_id ensures that the URL you're being redirected to is the pretty URL
status: 301 sets the redirect to the status of 301, for SEO
unless #item.friendly_id == params[:id] makes sure that we're not redirecting people who access #item through its pretty URL
just defined the redirection inside the routes file
get '/:old_id', to: redirect {|params, req| "/#{X.find(params[:old_id]).friendly_id}" }
While James Chevalier's answer is correct, you can extract this method to the ApplicationController in order to use with any model that uses FriendlyId:
def redirect_resource_if_not_latest_friendly_id(resource)
# This informs search engines with a 301 Moved Permanently status code that
# the show should now be accessed at the new slug. Otherwise FriendlyId
# would make the show accessible at all previous slugs.
if resource.friendly_id != params[:id]
redirect_to resource, status: 301
end
end
As you can see it's also unnecessary to pass a specific action key to redirect_to. Passing a Rails model to redirect_to will automatically attempt to access the show action on the associated collection resource route (assuming it's set up that way). That also means it's unnecessary to pass an id key since FriendlyId always returns the latest slug in the model's #to_param.
Not being a huge fan of unless (confusing semantics) I tend to shy away from it but that's more my personal preference.
Routes
I don't think your routes are the problem here
The problem is the backend handling of the route (I.E whether it uses friendly_id or not). All Google will see is this:
domain.com/users/45
domain.com/users/your_user
If both of those routes work, Google will be happy. I think you're alluding to the idea that if you change the routes to only handle your_user, you'll need to be able to get Google to appreciate the redirects
Redirects
Considering you can handle both id and slug in the backend (we have code for this if you want), I'd handle redirects using the ActionDispatch::Routing::Redirection class:
#config/routes.rb
begin
User.all.each do |u|
begin
get "#{u.id}" => redirect("#{u.slug}")
rescue
end
end
rescue
end
Yes it is possible, you need to define both routes on your config/routes.rb
get 'path/:id' => 'controller#action'
get 'path/:friendly_id' => 'controller#action_2'
then in your legacy action method you need to provide a
return redirect_to controller_action_2_path(friendly_id: friendly_id),
status: :moved_permanently
this will generate a 301 response code. Which will eventually make bots start hitting your new pattern, without losing any of your traffic or indexing (SEO).

Rails redirect_to post method?

redirect_to :controller=>'groups',:action=>'invite'
but I got error because redirect_to send GET method I want to change this method to 'POST' there is no :method option in redirect_to what will I do ? Can I do this without redirect_to.
Edit:
I have this in groups/invite.html.erb
<%= link_to "Send invite", group_members_path(:group_member=>{:user_id=>friendship.friend.id, :group_id=>#group.id,:sender_id=>current_user.id,:status=>"requested"}), :method => :post %>
This link call create action in group_members controller,and after create action performed I want to show groups/invite.html.erb with group_id(I mean after click 'send invite' group_members will be created and then the current page will be shown) like this:
redirect_to :controller=>'groups',:action=>'invite',:group_id=>#group_member.group_id
After redirect_to request this with GET method, it calls show action in group and take invite as id and give this error
Couldn't find Group with ID=invite
My invite action in group
def invite
#friendships = current_user.friendships.find(:all,:conditions=>"status='accepted'")
#requested_friendships=current_user.requested_friendships.find(:all,:conditions=>"status='accepted'")
#group=Group.find(params[:group_id])
end
The solution is I have to redirect this with POST method but I couldn't find a way.
Ugly solution: I solved this problem which I don't prefer. I still wait if you have solution in fair way.
My solution is add route for invite to get rid of 'Couldn't find Group with ID=invite' error.
in routes.rb
map.connect "/invite",:controller=>'groups',:action=>'invite'
in create action
redirect_to "/invite?group_id=#{#group_member.group_id}"
I call this solution in may language 'amele yontemi' in english 'manual worker method' (I think).
The answer is that you cannot do a POST using a redirect_to.
This is because what redirect_to does is just send an HTTP 30x redirect header to the browser which in turn GETs the destination URL, and browsers do only GETs on redirects
It sounds like you are getting tripped up by how Rails routing works. This code:
redirect_to :controller=>'groups',:action=>'invite',:group_id=>#group_member.group_id
creates a URL that looks something like /groups/invite?group_id=1.
Without the mapping in your routes.rb, the Rails router maps this to the show action, not invite. The invite part of the URL is mapped to params[:id] and when it tries to find that record in the database, it fails and you get the message you found.
If you are using RESTful routes, you already have a map.resources line that looks like this:
map.resources :groups
You need to add a custom action for invite:
map.resources :groups, :member => { :invite => :get }
Then change your reference to params[:group_id] in the #invite method to use just params[:id].
I found a semi-workaround that I needed to make this happen in Rails 3. I made a route that would call the method in that controller that requires a post call. A line in "route.rb", such as:
match '/create', :to => "content#create"
It's probably ugly but desperate times call for desperate measures. Just thought I'd share.
The idea is to make a 'redirect' while under the hood you generate a form with method :post.
I was facing the same problem and extracted the solution into the gem repost, so it is doing all that work for you, so no need to create a separate view with the form, just use the provided by gem function redirect_post() on your controller.
class MyController < ActionController::Base
...
def some_action
redirect_post('url', params: {}, options: {})
end
...
end
Should be available on rubygems.

Resources