Accessing Sinatra scope from another class - ruby

I'm running a Sinatra application with a few extra classes pulled in for creating a User and a few others on the fly (no DB, it feeds in from a web service). I'm trying send out a flash notice (using https://github.com/nakajima/rack-flash) from within my User model but can't figure out how to get access to the flash method/variable because I'm out of scope.
Something like:
class User
def something
if true
flash[:notice] = 'Good job'
else
# nope
end
end
end
Which gets required into the Sinatra app by a simple require 'models/user'

This is an XY Problem[1]. Sinatra is responsible for sending out flash messages, not your User objects, so the code for setting the flash should be in your Sinatra app, not in your User class.
[1] http://www.perlmonks.org/index.pl?node_id=542341

You should not ask your User (model) to talk to the UI (view). That's bad/not MVC-clean. That's what a controller is for.
You can use either return values, exceptions, or throw/catch (which is not exception handling) to pass information from your model to your controller. For example, using return values:
post "/create_user" do
flash[:notice] = case User.something
when User then "User Created!"
when :nono then "That's not allowed"
when :later then "User queued to be created later."
end
end
class User
def self.something
if authorized
if can_create_now
new(...)
else
queue_create(...)
:later
end
else
:nono
end
end
end
Since I mentioned them above, following are examples using throw/catch and begin/rescue (exceptions). As the advisability of using either of these constructs is questionable, let us take a moment of silence to ponder if this is a good idea.
Here is an example using throw/catch:
post "/create_user" do
result = catch(:msg){ User.something }
flash[:notice] = case
when :nono then "That's not allowed"
when :later then "User queued to be created later."
else "User Created!"
end
end
class User
def self.something
throw :msg, :nono unless authorized
if can_create_now
new(...)
else
queue_create(...)
throw :msg, :later
end
end
end
Finally, here's an example using exceptions, though I'm not convinced that this will be appropriate for all (non-disastrous) cases where you want to flash unique messages to the user:
post "/create_user" do
flash[:notice] = "User Created!" # Assume all good
begin
User.something
rescue User::Trouble=>e
flash[:notice] = case e
when Unauthorized then "That's not allowed"
when DelayedCreate then "User queued to be created later."
else "Uh...Something bad happened."
end
end
end
class User
class Trouble < RuntimeError; end
class Unauthorized < Trouble; end
class DelayedCreate < Trouble; end
def self.something
raise Unauthorized unless authorized
if can_create_now
new(...)
else
queue_create(...)
raise DelayedCreate
end
end
end
Exceptions let you pass an additional data along (e.g. raise Unauthorized.new "No such account", or any custom properties you want to add to your class), and so may be more useful (when appropriate). Just remember to pass semantic results from your model to your controller, and let it translate them into user-facing messages.

Related

Ruby - devise : confirmations_controller stop registrations_controller

I have a problem with devise I can't find the solution.
When a user sign_up, I need to call several services to make his profile. So here is the registrations_controller.rb.
require_relative '../../../app/services/affinities'
require_relative '../../../app/services/astroprofil'
require_relative '../../../app/services/geocode'
class Users::RegistrationsController < Devise::RegistrationsController
ASTROPROFIL = Astroprofil.new
AFFINITIES = Affinities.new
GEOCODE = Geocode.new
after_action :create_astroprofil, only: %i[new create]
after_action :create_affinities, only: %i[new create]
private
def create_astroprofil
return unless user_signed_in?
ASTROPROFIL.profil(current_user)
end
def create_affinities
return unless user_signed_in?
affinities(current_user, ten_mates)
end
def affinities(user, mates)
AFFINITIES.partner_report(user, mates)
AFFINITIES.sign_report(user, mates)
AFFINITIES.match_percentage(user, mates)
end
def ten_mates
mates_by_gender = User.where(gender: current_user.looking_for).where.not(id: current_user.id)
return mates_by_gender.sample(10)
end
end
When I sign up everything works perfectly, a new user is entirely created.
But as soon as I try to add a confirmation per mail with devise, the mails are sent but it stops the 'create_astroprofil' and the 'create_affinities' methods.
Do you have any idea about what's happening ?
I would say it's coming from this line
registrations_controller.rb#L28
Since you cannot login without having confirmed your email, I'm pretty sure create_astroprofil and create_affinities are called but their first line is return unless user_signed_in?.
2 options here:
Astroprofil.new and Affinities.new can be called for an unconfirmed user
Called create_astroprofil and create_affinities from ConfirmationController#show

How should I return Sinatra HTTP errors from inside a class where HALT is not available?

I have a large backend API for my native app that's built in Sinatra, that also serves some admin web pages. I'm trying to dry up the codebase and refactor code into classes inside the lib directory.
My API clients expect a status and a message, such as 200 OK, or 404 Profile Not Found. I'd usually do this with something like halt 404, 'Profile Not Found'.
What's the easiest way of halting with an HTTP status code and a message from inside a class?
Old Wet Code
post '/api/process_something'
halt 403, 'missing profile_id' unless params[:profile_id].present?
halt 404, 'offer not found' unless params[:offer_id].present?
do_some_processing
200
end
New Dry Code
post '/api/process_something'
offer_manager = OfferManager.new
offer_manager.process_offer(params: params)
end
offer_manager.rb
class OfferManager
def process_offer(params:)
# halt 403, 'missing profile_id' unless params[:profile_id].present?
# halt 404, 'offer not found' unless params[:offer_id].present?
# halt doesn't work from in here
do_some_processing
200
end
end
This question is probably better for CodeReview but one approach you can see in an OO design here is a 'halt' path and a 'happy' path. Your class just needs to implement a few methods to help this be consistent across all your sinatra routes and methods.
Here's one approach, and it would be easy to adopt this kind of interface across other classes using inheritance.
post '/api/process_something' do
offer_manager = OfferManager.new(params)
# error guard clause
halt offer_manager.status, offer_manager.halt_message if offer_manager.halt?
# validations met, continue to process
offer_manager.process_offer
# return back 200
offer_manager.status
end
class OfferManager
attr_reader :status, :params, :halt_message
def initialize(params)
#params = params
validate_params
end
def process_offer
do_some_processing
end
def halt?
# right now we just know missing params is one error to halt on but this is where
# you could implement more business logic if need be
missing_params?
end
private
def validate_params
if missing_params?
#status = 404
#halt_message = "missing #{missing_keys.join(", ")} key(s)"
else
#status = 200
end
end
def do_some_processing
# go do other processing
end
def missing_params?
missing_keys.size > 0
end
def missing_keys
expected_keys = [:profile_id, :offer_id]
params.select { |k, _| !expected_keys.has_key?(k) }
end
end

rspec 3 error when user is prompted for input

I have code that is requesting the user for input, such as:
class Foo
def prompt_for_foobar
puts "where is the foobar?"
gets.chomp
end
end
I would like to test that my application is asking "where is the foobar?". My test will pass upon commenting out 'gets.chomp'. but that is needed and the anything else I have tried has given me a Errno::ENOENT: error.
it "should prompt user" do
console = Foo.new
request = "where is the foobar?"
expect { console.prompt_for_foobar }.to output(request).to_stdout
end
What is the best way to test this method?
Not sure if this is the best way to handle this, but you can send puts and gets to STDOUT and STDIN.
class Foo
def prompt_for_foobar
STDOUT.puts "where is the foobar?"
STDIN.gets.chomp
end
end
Then, test that STDIN receives that puts message with your desired object.
describe Foo do
let(:foo) { Foo.new }
before(:each) do
allow(STDIN).to receive(:gets) { "user input" }
end
describe "#prompt_for_foobar" do
it "prompts the user" do
expect(STDOUT).to receive(:puts).with("where is the foobar?")
foo.prompt_for_foobar
end
it "returns input from the user" do
allow(STDOUT).to receive(:puts)
expect(foo.prompt_for_foobar).to eq "user input"
end
end
end
The problem is that gets is a method that forces human interaction (at least in the context of RSpec, where stdin isn't connected to a pipe from another process), but the entire point of automated testing tools like RSpec are to be able to run tests without involving human interaction.
So, rather than relying directly on gets in your method, I recommend you rely on a collaborator object that implements a particular interface -- that way in the test environment you can provide an implementation of that interface that provides a response without human interaction, and in other environments it can use gets to provide a response. The simplest collaborator interface here is probably a proc (they're perfect for this kind of thing!), so you could do the following:
class Foo
def prompt_for_foobar(&responder)
responder ||= lambda { gets }
puts "where is the foobar?"
responder.call.chomp
end
end
RSpec.describe Foo do
it 'prompts the user to respond' do
expect { Foo.new.prompt_for_foobar { "" } }.to output(/where is the foobar/).to_stdout
end
it "returns the responder's response" do
expect(Foo.new.prompt_for_foobar { "response" }).to eq("response")
end
end
Notice that prompt_for_foobar no longer calls gets directly; instead it delegates the responsibility of getting a response to a responder collaborator. By default, if no responder is provided, it uses gets as a default implementation of a responder. In your test you can easily provide a responder that requires no human interaction simply by passing a block that returns a string.

How to display payment gateway response messages in view - Rails 4, Active Merchant

I'm using Active Merchant with Stripe as the payment gateway. Everything works fine except that i don't know how to go about getting the gateway response error messages from Stripe (when a card is declined, invalid etc) to display on the checkout page to the user. I can get a StandardError to be raised that redirects to an error page with the response message but that's it.
ORDER MODEL
class Order < ActiveRecord::Base
has_many :order_products
has_many :products, through: :order_products
attr_accessor :card_number, :security_code, :card_expires_on
validate :validate_card, :on => :create
def validate_card
unless credit_card.valid?
credit_card.errors.full_messages.each do |message|
errors[:base] << message
end
end
end
def purchase(basket)
response = GATEWAY.purchase(Product.total_basket_price(basket)*100, credit_card, purchase_options)
unless response.success?
raise StandardError, response.message
end
end
def credit_card
#credit_card ||= ActiveMerchant::Billing::CreditCard.new(
:number => card_number,
:first_name => first_name,
:last_name => last_name,
:verification_value => security_code,
:month => card_expires_on.month,
:year => card_expires_on.year
)
end
def purchase_options
{
:billing_address => {
:address1 => address_1,
:address2 => address_2,
:city => city,
:country => country_code,
:zip => postal_code
}
}
end
end
ORDERS CONTROLLER
class OrdersController < ApplicationController
def create
#order = Order.new(order_params)
# #product = basket.find(params[:product_id])
basket.each do |item_id|
#order.order_products.build(product: Product.find(item_id))
end
if #order.save
if #order.purchase(basket)
render "show"
else
render "failure"
end
else
render "new"
end
end
Can anyone lend a hand, please??
Many Thanks
Easy peasy!
This is a simple matter of control flow. In Ruby, as in most languages, exceptions interrupt the normal program flow. As your code is written now, #purchase is raising an exception when it fails.
That's fine and a perfectly valid design decision. But the code interacting with #purchase is this:
if #order.purchase(basket)
render "show"
else
render "failure"
end
That code has no exception handling, so any exception will be caught by Rails, program flow will halt, and you'll get either a detailed error page (in development mode) or a generic 500 error page (in production mode).
Since you profess to be new to Ruby and Rails, a little code substitution might make this clearer:
# If #purchase is successful, it evaluates to true.
if true
render "show" # 'show' view is rendered as expected. Flow stops.
else
render "failure"
end
# If #purchase fails, it raises an exception.
if raise StandardError, response.message
# ^^^ Exception is raised, flow stops here.
render "show" # This code is never reached.
else # This code is never reached.
render "failure" # This code is never reached.
end
As I implied in the beginning, though, it's an easy fix once you know what the issue is. You can simply handle the exception with rescue. Where you currently have an if/else block, you can swap in an if block and a rescue block:
if #order.purchase(basket)
render 'show'
end
rescue => e
render 'failure'
There's room for improvement here depending on your needs—since you're raising and rescuing StandardError, for example, your can't easily distinguish between a network failure and a declined card—but it'll get you moving again.
After a lot of fiddling and help, the working solution was to search for an error key within the response params hash and if an error was present add the message to the object errors. Not particularly elegant but it now does what i want.
ORDER MODEL
def purchase(basket)
response = GATEWAY.purchase(Product.total_basket_price(basket)*100, credit_card, purchase_options)
if response.params.key?('error')
self.errors.add :base, response.message
false
else
true
end
end
ORDERS CONTROLLER
Also switched the order of the if statements in the controller so that def purchase(basket) runs first before the order is saved, allowing the error message(s) from the response to be caught and displayed.
if #order.purchase(basket)
if #order.save
render "show"
else
render "new"
end
else
render "new"
end
VIEW
<%= if #order.errors.any?
#order.errors[:base].to_sentence
end%>

is there any account recovery gem for ruby on rails that uses SMS?

I was wondering if there was any account recovery gem for a ruby on rails password allowing the app to send the user to his sms a pin to reset their password in the event the user forgets it? Googled but didn't see anything, figured I would ask here in case my google search string was just poorly written.
ruby on rails account recovery via sms
I'm not aware of any gem, but this sounds like something that wouldn't be terribly difficult to implement. As vgoff has mentioned, there are plenty of SMS services available to you.
Something along the lines of (haven't tested this):
class SMSReset < ActiveRecord::Base
TOKEN_LENGTH = 4
EXPIRY_TIME = 15.minutes
belongs_to :user
before_create :generate_token, :set_expiry
def dispatch_sms!
MySMSProvider.send_sms(to: user.mobile_number, body: "Your SMS token is: #{token}")
end
def has_not_expired?
expires_at > Time.now
end
private
def generate_token
self[:token] = SecureRandom.hex[0..TOKEN_LENGTH - 1].downcase
end
def set_expiry
self[:expires_at] = EXPIRY_TIME.from_now
end
end
class PasswordResetController < ApplicationController
def new
end
def create
#user = User.where(email: params[:email]).first
if #user
sms_reset = #user.create_sms_reset!
sms_reset.dispatch_sms!
flash.now[:success] = "Please enter the code that was sent to your phone in the field below"
else
flash.now[:error] = "No user was found by that email address"
render :new
end
end
def validate_token
sms_reset = SMSReset.where(user_id: params[:user_id], token: params[:token])
if sms_reset.present? && sms_reset.has_not_expired?
#user = sms_reset.user
render :password_reset_form
else
flash.now[:error] = "Sorry, that code wasn't recognized"
render :new
end
end
end
You'll want to handle errors, and there's room for improvement, but hopefully the gist makes sense.
Not that I am directly aware of, but https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=sms gives some gems for SMS interaction.
That is one of the first places I look, as well as searching on github.com directly. RubyForge is another good source of information for finding gems.
https://rubyforge.org/search/?type_of_search=soft&words=sms&Search=Search

Resources