I was demoing an app for a class I was teaching and ran into a bug. I was demo'ing POST requests in a Sinatra application. Here's my routes:
require "bundler/setup"
require "sinatra"
require "sinatra/reloader"
first_names = []
get '/' do
#first_names = first_names
erb :index
end
post '/add_name' do
first_names << params[:first_name]
redirect "/"
end
In the index.erb:
<h1>All Names</h1>
<% #first_names.each do |name| %>
<div><%= name %></div>
<% end %>
<h2>Enter New Name Here and Hit Enter</h2>
<form action="add_name" method="post">
<input name='first_name'>
<input type="submit" value="Add First Name"
</form>
Gemfile:
source 'https://rubygems.org'
gem "sinatra"
gem "sinatra-contrib"
Notice the unclosed input tag in the form. THIS actually works. Which is fine, HTML is buggy? But the part, that trips me up, is if I change the action of the form to addd_name(mispelled). It doesn't give me the error I expect. "Sinatra doesn't know this diddy...." It just does nothing. Any thoughts?
If you close the input tag and try to misspell action as '/addd_name' you'll get that ditty error :)
It that case, browser do not handle your "form" as a form. Check your network tab from developer console. You'll see that nothing happens when you click submit button.
Btw, if you've not declared (e.g. in configure do ... end block) first_names array, you'll get another error - nilClass or sth like that.
Related
I have basic Store app - I want to be able to add product to basket and then delete it using Sinatra. My adding works, but I can't make delete to work too. I had special function for that, but now I just want to see whether delete route works at all. My main app has many routes, and part I'm talking about is:
post '/basket' do #it works, adds to basket and redirects
AddToBasket.new(params).call
redirect '/'
end
delete "/basket/delete" do #it doesn't work at all and doesn't redirect
basket = BASKET.find{|p| p.id == params["id"]}
BASKET.delete(basket)
redirect "/"
end
In HTML I have:
<% basket.each do |b| %>
<form action="basket/delete" method="post">
<input type="hidden" name="_method" value="delete">
<input type="hidden" name="id" value=<%= b.id %>>
<button type="submit">Delete</button>
</form>
<% end %>
As you can see, after clicking on "Delete" button, I'm sending "id" in my params helper.
basket = BASKET.find{|p| p.id == params["id"]}
should find one specific item with this id and delete it from my big array BASKET. But it doesn't work, after clicking on "Delete" I'm otransferred to basket/delete page and I have an error, because post for basket/delete doesn't exist. It should redirect me to my index page. What's more, it doesn't delete my basket item, it still exists. I'll appreciate any help.
You need this component in your middleware pipeline use Rack::MethodOverride
Another way seems to be put set :method_override, true in your Sinatra::Base class
See this also
I'm trying to build an e-commerce site using Sinatra, as practice. I'm getting stumped on how to implement the 'Add to Cart' Button. My thought process about it is:
User clicks 'add to cart'
The button 'add to cart' invokes a ruby method, for example clicking on the following button
<input class='btn btn-primary' type='button' value='Add To Cart'></input>
should call a ruby method like
shop.add_to_cart(product, quantity)
An example of what this method might looking like:
class Shop
attr_reader :cart
def initialize
#cart = []
end
def add_to_cart(product, quantity)
#cart << product, quantity
end
end
In Rails, I think we use the helper_method in the controller? Is there anything similar I can do in Sinatra?
Thanks!
Note:
This is if you want to do it in ruby. You could probably also do it in javascript as mentioned in the other answer, but I cannot help you with that because I don't know javascript well enough.
To run the ruby method on button click you first need to create a <form> with only the button, then have that run a route in your app file that will run the method then redirect back to the page you were on. Here is my code (have not tested):
home.erb:
<form method="post" action="/runMethod">
<input type="hidden" name="product" value="whatever">
<input type="hidden" name="quantity" value="whatever">
<input class='btn btn-primary' type='submit' value='Add To Cart'>
</form>
You would set the values of the two hidden inputs (where I wrote "whatever") to the quantity and product according to their names.
App File:
class Shop
attr_reader :cart
def initialize
#cart = []
end
def add_to_cart(product, quantity)
#cart << product, quantity
end
end
get '/' do
erb :home
end
post '/runMethod' do
shop.add_to_cart(params[:product], params[:quantity])
redirect '/'
end
This can also be accomplished with ajax so that you dont have to leave the page:
$("#hideCarousel").submit(function() {
//posts the contents of the form to /action using ajax
$.post("/action", $("#myform").serialize(), function(result){
// assuming result is a string of the updated data in html
// and assuming that your data goes in an element with the id data-table
$("#data-table").html(result)
});
return false; // prevents the form from submitting normally
});
Rails/Sinatra run on the server side. If you want stuff happening in Rails directly you probably need a form and post back data.
nowadays people use javascript and it's javascript that makes the callbacks in an asynchronous fashion for this kinds of things.
I need to call method download_images("\folder","http:\url") which save pictures from url in choosen directory.This method should be called in index.html.erb and grab folder address from textbox1 and url from textbox2 after pressing the button1.
Right now I don't know how to grab strings from textboxes, I am trying to call method correctlyThe index.html.erb code:
<h1>Welcome#index</h1>
<p><%= "Download pictures from url!" %></p>
<div class="form-horizontal">
<p> Input url: </p>
<p> <input type="text"/> </p>
<p> Input destination folder: </p>
<p> <input type="text"/> </p>
<button class="btn">Go!</button>
<% button_to "btn", :method=> download_images("`/tutorial1/downloadedpics","http://www.yandex.ru/") %>
</div>
I defined method download_images in welcome_controller.rb:
class WelcomeController < ApplicationController
def index
end
def download_images(url, destination_path, options = {})
base_url = URI.join(url, "/").to_s
body = Typhoeus::Request.get(url).body
imgs = Nokogiri::HTML(body).css("img")
image_srcs = imgs.map { |img| img["src"] }.compact.uniq
hydra = Typhoeus::Hydra.new(:max_concurrency => options[:max_concurrency] || 50)
image_paths = image_srcs.map do |image_src|
image_url = URI.join(base_url, image_src).to_s
path = File.join(destination_path, File.basename(image_url))
request = Typhoeus::Request.new(image_url)
request.on_complete { |response| File.write(path, response.body) }
hydra.queue(request)
path
end
hydra.run
image_paths
end
end
After I switch server and go to localhost, I receive an exception:
NoMethodError in Welcome#index, undefined method download_images' for #<#<Class:0x007f202fc3ae50>:0x007f202f9ab518>, in line <% button_to "btn", :method=> download_images("/tutorial1/downloadedpics","http://www.yandex.ru/") %>
I am a noob programmer, so I can do rather dumb mistakes...
And it is important to mention: I work in Nitrous web box, and don't really know is it possible to download images in box folder:
~/tutorial1/downloadedpics
Also I use Bootstrap controllers,Nokogiri gem and Typhoeus gem.
Ruby version:ruby 2.1.1p76
Rails version:Rails 4.1.0
Thank you for your attention.
As a FYI, doing:
imgs = Nokogiri::HTML(body).css("img")
image_srcs = imgs.map { |img| img["src"] }.compact.uniq
is not the right way to find images with "src" parameters. Because you're not searching correctly, you get nils in your resulting array, forcing you to use compact. Instead, don't rely on cleaning up after making a mess, just avoid making the mess in the first place:
require 'nokogiri'
body = <<EOT
<html>
<body>
<img>
<img src="foo">
</body>
</html>
EOT
imgs = Nokogiri::HTML(body).css("img[#src]")
image_srcs = imgs.map { |img| img["src"] }
image_srcs # => ["foo"]
I've just started "Build Your Own Ruby on Rails" and I have had to use Google a lot, as the book seems to have a bunch of places where the code just doesn't work. This time, I couldn't find an answer. Okay, so here's the deal. I have a form that looks like this:
new.html.erb:
<%= form_for :story do |f| %>
<p>
name:<br />
<%= f.text_field :name %>
</p>
<p>
link: <br />
<%= f.text_field :link %>
</p>
<p>
<%= submit_tag %>
</p>
<% end %>
It shows up fine when I go to localhost:3000/story/new. The thing is, when I try to type stuff into the form and press "submit," I get this error:
Routing Error
No route matches [POST] "/story/new"
My routes.rb looks like this:
FirstApp::Application.routes.draw do
resources :story
story_controller looks like this:
def new
#story = Story.new(params[:story])
if request.post?
#story.save
end
end
The story_controller stuff for new is straight out of the book. I thought I might have had a solution here, but no dice. Any help would be greatly appreciated.
I'm guessing you meant (note the at sign):
<%= form_for #story do |f| %>
That'll probably take care of your routing issue, but as John mentions, your controller action is a bit off, too. The new action should only load a dummy model and display the new.html.erb page - the saving should take place in a separate action, called create.
Hope this helps!
Edit: Minimal controller code:
class StoriesController < ApplicationController
def new
#Make a dummy story so any default fields are filled correctly...
#story = Story.new
end
def create
#story = Story.new(params[:story])
if(#story.save)
#Saved successfully; go to the index (or wherever)...
redirect_to :action => :index
else
#Validation failed; show the "new" form again...
render :action => :new
end
end
end
First off, Rails is relies on convention over configuration when using singular vs plural names. If you want to follow convention, you have to change the line in your routes.rb to resources :stories, which would generate following routes:
stories GET /stories(.:format) stories#index
POST /stories(.:format) stories#create
new_story GET /stories/new(.:format) stories#new
edit_story GET /stories/:id/edit(.:format) stories#edit
story GET /stories/:id(.:format) stories#show
PUT /stories/:id(.:format) stories#update
DELETE /stories/:id(.:format) stories#destroy
Note, that in this case you would have to rename your controller to StoriesController. However, your routes.rb has resources :story, which generates following routes:
story_index GET /story(.:format) story#index
POST /story(.:format) story#create
new_story GET /story/new(.:format) story#new
edit_story GET /story/:id/edit(.:format) story#edit
story GET /story/:id(.:format) story#show
PUT /story/:id(.:format) story#update
DELETE /story/:id(.:format) story#destroy
As you can see, indeed, there is no route for POST /story/new. I guess, the error that you are getting is triggered by following code in your controller:
if request.post?
#story.save
end
It is quite wrong, because you trying to check for POST request inside the action that is routed to by GET. Just remove this code from your new action and add create action to your StoryController like this:
def create
#story = params[:story]
if #story.save
redirect_to #story, notice: "Story created"
else
render action: "new"
end
end
This should resolve your issue for now. But I strongly recommend using plural stories for your resources, since it will be back to haunt you again.
This is the part that you (and me) have missed from the guide:
There's one problem with this form though. If you inspect the HTML
that is generated, by viewing the source of the page, you will see
that the action attribute for the form is pointing at /articles/new.
This is a problem because this route goes to the very page that you're
on right at the moment, and that route should only be used to display
the form for a new article.
The form needs to use a different URL in order to go somewhere else.
This can be done quite simply with the :url option of form_for.
Typically in Rails, the action that is used for new form submissions
like this is called "create", and so the form should be pointed to
that action.
Edit the form_for line inside app/views/articles/new.html.erb to look like this:
<%= form_for :story, url: stories_path do |f| %>
I am building a Sinatra app and wrote it linearly (no methods) to learn how Sinatra works. Now I am trying to refactor it, but the params from my form submission aren't being passed to the methods. Here are the routes:
get '/' do
erb :index
end
post '/' do
session = login(params[:username], params[:password])
get_courses(session, params[:username])
erb :index
end
And here is index.erb
<% if !#courses %>
<form action="/" method="post">
<input type="text" label="username" name="username">
<input type="password" label="password" name="password">
<input type="submit">
</form>
<% end %>
<% if #courses %>
<ul>
<% #courses.each do |course| %>
<li><%= course %></li>
<% end %>
</ul>
<% else %>
<p>No course data yet.</p>
<% end %>
I know the params are being passed to Sinatra because I was able to do a simple puts of them, but I get an "ArgumentError at / wrong number of arguments (0 for 2)" when I try to pass them to the methods.
Update
From the same file as the routes, here is the login method:
def login(username, password)
login = Savon::Client.new($LOGIN_WSDL)
login.http.auth.ssl.verify_mode = :none
session = login.request(:login) do
soap.body = { :id => username, :pw => password }
end
session.to_hash
end
Apologies if this is obvious, but have you defined #courses in any of your methods?
Something like this:
def get_courses(session, username)
# ...
#some logic to figure out courses based on session and username
# ...
#courses = ["Some course", "Another course"]
end
I just ran into this locally as well.
I found that, for reasons yet unclear to me, the keys in the #params Hash are always Strings rather than Symbols. The Sinatra documentation seems to reflect that the Hash is either a HashWithIndifferentAccess, or the keys are automatically symbolized as part of the #params method.
Not sure if that is the same issue you are experiencing, if it's an unintended behavior or Sinatra, or I'm just bad at reading docs.