Use a here string and lambda in a Sinatra helper method - ruby

I'm trying to create a Sinatra helper that returns a dynamically-generated HTML. I thought I would use a here string for the static bit and a lambda to calculate the dynamic part.
foo_helper.rb:
require 'erb'
module FooHelper
def tabs(selected)
template = ERB.new <<~HERE
<ul class="nav nav-tabs">
<li class="nav-item"><a class="nav-link <%= 'active' if selected == 'favorites' %>" href="/foo/favorites">Favorites</a></li>
<li class="nav-item"><a class="nav-link <%= 'active' if selected == 'all' %>" href="/foo">All</a></li>
<%= alpha.call %>
</ul>
HERE
# binding to a string works as expected
# alpha = "<li class='nav-item'><a class='nav-link' href='/foo/a'>A</a></li>"
# binding to a lambda, doesn't
alpha = lambda {
('a'..'z').each do |letter|
"<li class='nav-item'><a class='nav-link #{ 'active' if selected == letter }' href='/foo/#{letter}'>#{letter}</a></li>"
end
}
template.result(binding)
end
end
foo_controller.rb:
class FooController < ApplicationController
helpers FooHelper if defined?(FooHelper)
...
end
index.erb:
...
<%= tabs('favorites') %>
...
Results:
Displays the range, rather than the individual lis.
Am I missing something in the lambda?
** edit **
Corrected the numerous errors.

There are just too many mistakes in your code.
#nav is defined at module level, but accessed at instance level, so you got nil when you need it.
When you call a lambda, you need a dot between the variable name and the opening parenthesis, like foo.(123)
#nav.foo(binding), really? What's the (supposed) type of #nav? Does that type have the instance method foo?
<%= foo %> won't execute foo, because it's a local variable, not a method.

The reason why it renders a..z is because the Range#each method (called from within the lambda) executes a given block for each element and then returns the range itself again.
What you would want to use here instead is the Enumerable#map method. Similar to #each it also executes a block for each element, but the return values of said block are returned in a new array.
For comparison:
p ("a".."c").each { |x| x.upcase }
#=> "a".."c"
p ("a".."c").map { |x| x.upcase }
#=> ["A", "B", "C"]

Related

undefined method with googlebooks

I'm building a library in sinatra, using postresql as database and a 'googlebooks' gem to find the informations I want.
This is my code in main.rb
get '/list' do
#books = GoogleBooks.search(params[:title])
erb :list
end
get '/info/:isbn' do
#this is to get the info from my database if there is any
if book = Book.find_by(isbn: params[:isbn])
#book_title = book.title
#book_authors = book.author
#book_year = book.year
#book_page_count = book.page_count
#book_id = book.id
#book_isbn = book.isbn
else
#use the gem to look for the data
text = GoogleBooks.search(params[:isbn])
#book_title = text.title
#book_author = text.authors
#book_year = text.published_date
#book_cover = text.image_link(:zoom => 2)
#book_page_count = text.page_count
#book_notes = text.description
#and then store it into the database
book = Book.new
book.title = #book_title
book.authors = #book_authors
book.publish_date = #book_year
book.image_link = #book_cover
book.page_count = #book_page_count
book.description = #book_notes
book.isbn = params[:isbn]
book.save
#book_id = book.id
end
erb :info
end
This is my erb file :list
<div class='list_by_title'>
<% #books.each do |text| %>
<ul>
<li class='list_by_title'>
<%= text.title %> (Authour: <%= text.authors %>)
</li>
</ul>
<%end%>
</div>
The list part works.. I'm able to have a page with a list of titles... the problem is when I try to call the data from the params isbn, I always have this error:
NoMethodError at /info/0596554877
undefined method `title' for #<GoogleBooks::Response:0x007ff07a430e28>
Any idea for a possible solution?
As per documentation, GoogleBooks::Response is an enumerator. So you need to iterate over it by using each, and invoke the title method on individual objects retrieved from enumerator.
result = GoogleBooks.search(params[:isbn])
result.each do |text|
#book_title = text.title
...
end
Variable name text for details about a book is not a good name, you need to pick apt variable names, many a times good variable names makes debugging easier

How to define custom locating strategy for select

I looking for a proper way to redefine/extend locating strategy for select tag in Gwt app.
From html snippet you can see that select tag is not visible.
So to select option from list I need to click on button tag, and than select needed li tag from dropdown.
<div class="form-group">
<select class="bootstrap-select form-control" style="display: none; locator='gender">
<div class="btn-group">
<button class="dropdown-toggle" type="button" title="Male">
<div class="dropdown-menu open">
<ul class="dropdown-menu inner selectpicker" role="menu">
<li data-original-index="1"> (contains a>span with option text)
.....more options
</ul>
</div>
</div>
</div>
I see dirty solution: to implement method in BasePage class. This approach nice page_object sugar(options,get value, etc):
def set_nationality(country, nationality='Nationality')
select = button_element(xpath: "//button[#title='#{nationality}']")
select.click
option = span_element(xpath: "//span[.='#{country}']")
option.when_visible
option.click
end
Is there any other more clear way to do so? Using `PageObject::Widgets maybe?
UPD: Here what I expect to get:
def bool_list(name, identifier={:index => 0}, &block)
define_method("#{name}_btn_element") do
platform.send('button_for', identifier.clone + "//button")
end
define_method("#{name}?") do
platform.send('button_for', identifier.clone + "//button").exists?
end
define_method(name) do
return platform.select_list_value_for identifier.clone + '/select' unless block_given?
self.send("#{name}_element").value
end
define_method("#{name}=") do |value|
return platform.select_list_value_set(identifier.clone + '/select', value) unless block_given?
self.send("#{name}_element").select(value)
end
define_method("#{name}_options") do
element = self.send("#{name}_element")
(element && element.options) ? element.options.collect(&:text) : []
end
end
The select list appears to have the most identify attributes, therefore I would use it as the base element of the widget. All of the other elements, ie the button and list items, would need to be located with respect to the select list. In this case, they all share the same div.form-group ancestor.
The widget could be defined as:
class BoolList < PageObject::Elements::SelectList
def select(value)
dropdown_toggle_element.click
option = span_element(xpath: "./..//span[.='#{value}']")
option.when_visible
option.click
end
def dropdown_toggle_element
button_element(xpath: './../div/button')
end
def self.accessor_methods(widget, name)
widget.send('define_method', "#{name}_btn_element") do
self.send("#{name}_element").dropdown_toggle_element
end
widget.send('define_method', "#{name}?") do
self.send("#{name}_btn_element").exists?
end
widget.send('define_method', name) do
self.send("#{name}_element").value
end
widget.send('define_method', "#{name}=") do |value|
self.send("#{name}_element").select(value)
end
widget.send('define_method', "#{name}_options") do
# Since the element is not displayed, we need to check the inner HTML
element = self.send("#{name}_element")
(element && element.options) ? element.options.map { |o| o.element.inner_html } : []
end
end
end
PageObject.register_widget :bool_list, BoolList, :select
Notice that all locators are in relation to the select list. As well, notice that we use the accessor_methods to add the extra methods to the page object.
The page object would then use the bool_list accessor method. Note that the identifier is for locating the select element, which we said would be the base element of the widget.
class MyPage
include PageObject
bool_list(:gender, title: 'Gender')
bool_list(:nationality, title: 'Nationality')
end
The page will now be able to call the following methods:
page.gender_btn_element.click
page.gender_btn_element.exists?
page.gender
page.gender = 'Female'
page.gender_options
page.nationality_btn_element.click
page.nationality_btn_element.exists?
page.nationality
page.nationality = 'Barbados'
page.nationality_options

How to convert a hash into a string in ruby

I'm trying to do an wolfram api using Ruby. I found that you can create a hash from text you put to find an answer on wolfram page. I managed to do something like this in my controller:
class CountController < ApplicationController
def index
#result = Wolfram.fetch('6*7')
#hash = Wolfram::HashPresenter.new(#result).to_hash
#pods = #hash[:pods]
end
end
When I want to show this on my site I do something like this in my view:
<p>
<b>Result:</b>
<%= #result %>
<br>
<b>Hash:</b>
<%= #hash %>
<br>
<b>Hash.pods</b>
<%= #pods["Input"]%>
<br>
</p>
And I have something like this on my page:
Result: #<Wolfram::Result:0x00000004758b78>
Hash: {:pods=>{"Input"=>["6×7"], "Result"=>["42"], "Number name"=>["forty-two"], "Number line"=>[""], "Illustration"=>["6 | \n | 7"]}, :assumptions=>{}}
Hash.pods ["6×7"]
I'd like to have just 6x7 instead of ["6x7"]. Is there a solution to change this hash into a string?
The reason why it is being displayed like [6x7] is that your hash stores it within an array. Displaying it any other way will be misleading. However you can do it with:
Hash[#hash.map {|key, value| [key, (value.kind_of?(Array) && value.size == 1) ? value.first : value }]

Array of Ruby objects returning strings on each method. Why?

Useful additional info: I am using the decent_exposure gem so this might be the issue - correcting the code below:
expose(:get_filter_tags) do
if params[:filter_tag_names]
filter_tag_names = Array(params[:filter_tag_names].split(" "))
filter_tags = Array.new
filter_tag_names.each do |f|
t = Tag.find_by_name(f)
filter_tags << t
end
end
end
So, something funny happens when I call this in the view:
query string ?utf8=✓&filter_tag_names=test
<% get_filter_tags.each do |ft| %>
<%= ft.name %>
<% end %>
Error message: undefined method `name' for "test":String
Why is this trying to call name on a string not a Tag object? If I put the following in the view, and have jut one filter_tag_names item
def getfiltertag
Tag.find_by_name(params[:filter_tag_names])
end
#view
<%= getfiltertag.name %>
query string: ?utf8=✓&filter=test
like above then I can call name just fine, so obviously I am doing something wrong to get an array of strings instead of objects. I just don't know what. Any suggestions?
Your problem is that each returns self — so if you write filter_tag_names.each, it returns filter_tag_names. You could fix this by explicitly returning filter_tags, but more idiomatically, you could just rewrite it as:
expose(:get_filter_tags) do
if params[:filter_tag_names]
filter_tag_names = Array(params[:filter_tag_names].split(" "))
filter_tag_names.map {|f| Tag.find_by_name(f) }
end
end
Just as an aside, this method will return nil if there aren't any filter tag names. You may want to do that, or you might want to return an empty collection to avoid exceptions in the calling code.

How to build nested menu "trees" in HAML

I am trying to build a simple nested html menu using HAML and am not sure how to go about inserting the elements with the correct indentation, or the general best way to build nested trees. I would like to be able to do something like this, but infinitely deep:
- categories.each_key do |category|
%li.cat-item{:id => "category-#{category}"}
%a{:href => "/category/#{category}", :title => "#{category.titleize}"}
= category.titleize
It feels like I should be able to accomplish this pretty easily without resorting to writing the tags by hand in html, but I'm not the best with recursion. Here is the code I've currently come up with:
View Helper
def menu_tag_builder(array, &block)
return "" if array.nil?
result = "<ul>\n"
array.each do |node|
result += "<li"
attributes = {}
if block_given?
text = yield(attributes, node)
else
text = node["title"]
end
attributes.each { |k,v| result += " #{k.to_s}='#{v.to_s}'"}
result += ">\n"
result += text
result += menu_tag_builder(node["children"], &block)
result += "</li>\n"
end
result += "</ul>"
result
end
def menu_tag(array, &block)
haml_concat(menu_tag_builder(array, &block))
end
View
# index.haml, where config(:menu) converts the yaml below
# to an array of objects, where object[:children] is a nested array
- menu_tag(config(:menu)) do |attributes, node|
- attributes[:class] = "one two"
- node["title"]
Sample YAML defining Menu
menu:
-
title: "Home"
path: "/home"
-
title: "About Us"
path: "/about"
children:
-
title: "Our Story"
path: "/about/our-story"
Any ideas how to do that so the output is like this:
<ul>
<li class='one two'>
Home
</li>
<li class='one two'>
About Us
</li>
</ul>
...not like this:
<ul>
<li class='one two'>
Home</li>
<li class='one two'>
About Us</li>
</ul>
... and so it's properly indented globally.
Thanks for the help,
Lance
The trick to nicely-indented, Ruby-generated Haml code is the haml_tag helper. Here's how I'd convert your menu_tag method to using haml_tag:
def menu_tag(array, &block)
return unless array
haml_tag :ul do
array.each do |node|
attributes = {}
if block_given?
text = yield(attributes, node)
else
text = node["title"]
end
haml_tag :li, text, attributes
menu_tag_builder(node["children"], &block)
end
end
end
How about something along the lines of:
def nested_list(list)
return unless list
haml_tag :ul do
list.each do |item|
haml_tag :li do
haml_concat link_to item["title"], item["path"]
if item["children"]
nested_list item["children"]
end
end
end
end
end
Awesome, #shingara's hint put me in the right direction :). This works perfectly:
def menu_tag(array, &block)
return "" if array.nil?
haml_tag :ui do
array.each do |node|
attributes = {}
if block_given?
text = yield(attributes, node)
else
text = node[:title]
end
haml_tag :li, attributes do
haml_concat text
menu_tag_builder(node[:children], &block)
end
end
end
end
If somebody can make that even shorter, or make it more easy to customize the attributes on the nested nodes, I'll mark that as correct instead of this.
Cheers.
It's because you send a pur HTML by your helper. The indentation become with HAML. You can can generate some HAML in your helper.

Resources