I want to write a simple erb template generator to parse stored erb templates from the views using Generator module. I call the Generator from the rails controller to generate it's singleton instances and pass it the WallController by self pointer.
require 'generator'
class WallController < ApplicationController
def index
header = File.read 'app/views/application-header.html'.freeze
#instances = {header: header}
# Load view generators
Generator.generate_instances self
end
end
The first thing Generator.generate_instances actually attempts to do is to copy the WallController instance variables (hence the self pointer) to perform correct parsing of the erb templates. Then it generates methods returning erb resulted text.
require 'erb'
module Generator
def self.generate_instances environment
# Mimic class environment
if environment.superclass == ApplicationController
environment.instance_variables.each do |v|
puts "Copy instance variable '#{v}' from #{environment.name} to #{self.name}"
value = environment.instance_variable_get(v)
self.send :instance_variable_set, v, value
end
end
# Parse the ERB templates
templates = #instances
return 0 if !templates.is_a?(Hash) or templates.empty?
templates.keys.each.with_index do |key, index|
define_singleton_method key do
ERB.new(templates.values[index]).result
end
end
end
end
Usage of Generator interface will look like this:
<%=== Generator.header %>
I am new to rails but I have found out that rails controller's included files are limited to a single static structure. I didn't manage either to overwrite class Object or class Class singleton methods which could be helpful.
However, after running the above example the instance variables of WallController return the WallController class address in stead of values defined by WallController.index.
undefined method `empty?' for #<WallController:0x000000000a1f90>
Is there a correct way to distribute a rails controller instance variables among other controllers? If not, why are regular instance copy not working?
If I had to write it in ruby, that would be easy:
module Y
def self.generate_environment environment
environment.instance_variables.each do |v|
puts "Copy #{v} from #{environment.name} to #{self.name}"
value = environment.instance_variable_get v
self.instance_variable_set(v, value)
end if environment.class == Class
puts "Retrived string: #{#hello}"
end
end
class X
def self.index
#hello = 'Hello, World!'
Y.generate_environment self
end
end
X.index
This problem may be solved with viewcomponent, which allows for standard ruby code for the view controller. Also solves the problem of dividing the view code to smaller reusable components in reasonable speed.
To use the viewcomponent gem first include it to your Gemfile.
gem 'view_component', require: 'view_component/engine'
After updating your gems with bundle install, you will also need to restart your server if it's running, to apply the new gem.
Then generating the component is similar in usage to other rails generators. The first argument is a component name and the second is a component argument.
rails generate component Header site_id
Now I focus on files generated in app/component directory, view and controller code. This will simply be the controller to create the header snippet of the view.
Inside of app/component/header_component.rb can be encapsulated all the code from WallController related to the header view.
class HeaderComponent < ViewComponent::Base
def initialize(site_id:)
puts "Rendering header component for site: #{site_id}"
# Load site elements
#site = Site.find site_id
#menu_items = []
Site.all.each.with_index do |site, index|
#menu_items.push site.title => site.link
end
end
end
Similarly, put all the header view erb code to the app/component/header.html.erb.
The finished component can be generated from the view using rails render:
<%= render HeaderComponent.new(site_id: 1) %>
Related
I'm using the Etsy gem, which as a Listing module.
I also have a corresponding Listing model in my app.
I'm trying to set up a sidekiq worker to work with the gem, call upon some methods and update the corresponding Listing row, but because I have include Etsy so that I can use the gem, rails gets confused and thinks I'm referring to the module instead of the model.
Here's the code:
class ListingWorker
include Sidekiq::Worker
include Etsy
def perform(seller, shop)
access_token = {access_token: seller.oauth_token,
access_secret: seller.request_secret}
myself = Etsy.myself(access_token[:access_token], access_token[:access_secret])
limit = 100
offset = 0
total_results = myself.shop.active_listings_count
until offset > total_results
listings = Etsy::Request.get("/shops/#{shop.shop_id}/listings/active",
access_token.merge(limit: limit.to_s,
offset: offset.to_s,
include_private: 'true',
includes: 'Images:1:0'))
.to_hash
offset += limit
listings['results'].each do |l|
listing = Listing.find_by(listing_id: l['listing_id'])
end
end
Here's the error I get:
undefined method `find_by' for Etsy::Listing:Class
How do I differentiate between the module and the model and make rails understand I mean a db table?
Thanks in advance!
The short answer to your problem is simply not to include Esty:
class ListingWorker
include Sidekiq::Worker
def perform(seller, shop)
# ...
end
end
The purpose of include is to add methods defined with the module into your class.
For example, if I defined a module:
module Tom
def hello
puts "Hello!"
end
end
And you wanted to call hello directly within your ListingWorker class, then you'd need to include Tom.
On the other hand, suppose I just define a "namespaced" method within a module, such as:
module Maayan
def self.example
puts "Example"
end
end
Then, this should be invoked by referencing the module - i.e. Maayan.example.
You can do this from anywhere (provided the file containing this code is loaded); you don't need to include anything.
And that's what you're doing here -- in order to run code such as Etsy.myself or Etsy::Request.get, you don't need to include the module. You're calling methods on the module directly, not mixing the module's methods into your own class.
However, if you did find yourself in such a situation, where you have a nested class which conflicts with the top-level definition, note that ruby lets you explicitly access the globally scoped class by prepending :: to the class name.
In other words, you can use ::Listing to explicitly reference your own class.
I'm trying to write a rails app that creates an object in the controller based on a helper module, which is written below:
module StockPricesHelper
require 'net/http'
class Stock
attr_accessor(:data)
def initialize(stock)
#url = "http://finance.yahoo.com/d/quotes.csv?s=#{stock}&f=sb2b3jk"
end
def download_data
#data = NET::HTTP.get_response(URI.parse(#url)).body
end
def clean_string
#data = #data.strip
end
def db_format
1
end
end
end
I get an error uninitialized constant StockPricesHelper::Stock::NET from the rails server.
Am I correctly putting this in a helper module?
What am I doing wrong? I think I'm off on the scope but I don't know where.
You have misspelled the "NET" module. It is Net. (Ruby is case sensitive)
Rails helpers are intended to be view helpers, i.e. aid in generating HTML.
It looks like you are performing something which would be better placed in a controller or background job.
I've tried Facebook's Open Graph protocol in adding meta data on Rails pages. What I want to do now is to make my code not duplicated or D.R.Y.---instead of putting one meta-data header for each controller page I have, I'd like to create a base class called "MyMetaBuilder" which will be inherited by the sub-pages, but don't know where and how to start coding it...
Someone suggested that meta data property values must be dynamically generated depending on the context. For example, PlayMetaBuilder, CookMetaBuilder and so on...
Also, when unit testing the controller action, how do I verify for its existence?
Thanks a lot.
One thing is defining the tags, another is rendering them. I would do the following:
write a controller mixin (something like acts_as_metatagable) where I would define specific fields for each controller (and populate the remaining with defaults). These would be assigned to a class (or instance) variable and in this way be made accessible in the rendering step).
write an helper function which would take all my tags and turn them into html. This helper function would then be called in the layout and be rendered in the head of the document.
so, it would look a bit like this:
# homepage_controller.rb
class HomepageController < ActionController::Base
# option 1.2: include it directly here with the line below
# include ActsAsMetatagable
acts_as_metatagable :title => "Title", :url => homepage_url
end
# lib/acts_as_metatagable.rb
module ActsAsMetatagable
module MetatagableMethods
#option 2.2: insert og_tags method here and declare it as helper method
def og_metatags
#og_tags.map do |k, v|
# render meta tags here according to its spec
end
end
def self.included(base)
base.helper_method :og_tags
end
end
def acts_as_metagabable(*args)
include MetatagableMethods
# insert dirty work here
end
end
# option 1.1: include it in an initializer
# initializers/acts_as_metatagable.rb
ActiveController::Base.send :include, ActsAsMetatagable
# option 2.1: insert og_metatags helper method in an helper
module ApplicationHelper
def og_metatags
#og_tags.map do |k, v|
# render meta tags here according to its spec
end
end
end
What I did for Scoutzie, was put all metadata into a head partial, with if/else cases as such:
%meta{:type => 'Author', :content => "Kirill Zubovsky"}
%meta{'property' => "og:site_name", :content=>"Scoutzie"}
-if #designer
...
-elsif #design
...
-else
...
This way, depending on the variables that load, I know which page it is, and thereby know which metadata to include. This might not be an elegant solution, but it works and it's really simple.
I have a setup in the lib directory like so:
lib/
copy_process.rb
copy_process/
processor.rb
The copy_process.rb and processor.rb contain the module definition CopyProcess. The copy_process.rb defines the CopyFile class as well:
module CopyProcess
class CopyFile
end
end
The processor.rb is structured like so:
module CopyProcess
class Processer
end
end
In one of its methods, it creates a new copy file object:
def append_file_if_valid(file_contents, headers, files, file_name)
unless headers
raise "Headers not found"
else
files << CopyProcess::CopyFile.new()
end
end
When I used these files as part of a command line ruby program, it worked fine. However, i started putting it into a rails app, and I have written cucumber/capybara tests to hit the buttons and so forth where this is used. I initialize a Processor object from one of my AR models, and call the above method a few times. It cannot seem to find the CopyFile class, even though I have the following code in my application.rb
config.autoload_paths += %W(#{config.root}/lib)
config.autoload_paths += Dir["#{config.root}/lib/**/"]
Any ideas?
===============================================================
Edit Above was solved by extracting the copy file class into it's own file under lib.
I now have another issue:
The CopyFile class refers to module-level helper methods that sit in lib/copy_process.rb, like so:
module CopyProcess
# Gets the index of a value inside an array of the given array
def get_inner_index(value, arr)
idx = nil
arr.each_with_index do |e, i|
if e[0] == value
idx = i
end
end
return idx
end
def includes_inner?(value, arr)
bool = false
arr.each { |e| bool = true if e[0] == value }
return bool
end
# Encloses the string in double quotes, in case it contains a comma
# #param [String] - the string to enclose
# #return [String]
def enclose(string)
string = string.gsub(/\u2019/, '’')
if string.index(',')
return "\"#{string}\""
else
return string
end
end
end
When I run my cucumber tests, i get the following error:
undefined method `includes_inner?' for CopyProcess:Module (NoMethodError)
./lib/copy_process/copy_file.rb:64:in `set_element_name_and_counter'
Which refers to this method here:
def set_element_name_and_counter(element_names, name)
if !CopyProcess::includes_inner?(name, element_names)
element_names << [name, 1]
else
# if it's in the array already, find it and increment the counter
current_element = element_names[CopyProcess::get_inner_index(name, element_names)]
element_names[CopyProcess::get_inner_index(name, element_names)] = [current_element[0], current_element[1]+1]
end
element_names
end
I also tried moving the copy_file.rb and other files in the lib/copy_process/ directory up a level into the lib directory. I then received the following error:
Expected /Users/aaronmcleod/Documents/work/copy_process/lib/copy_file.rb to define CopyFile (LoadError)
./lib/processor.rb:48:in `append_file_if_valid'
The line that the error states creates an instance of CopyFile. I guess rails doesn't like loading the files in that fashion, and for the former setup, I think the copy_file.rb is having issues loading the rest of the module. I tried requiring it and so forth, but no luck. You can also find my most recent code here: https://github.com/agmcleod/Copy-Process/tree/rails
First config.autoload_paths += %W(#{config.root}/lib) should be sufficient. This tells rails to start looking for properly structured files at /lib.
Second, I think that you're running into issues because CopyFile isn't where rails expects it to be. As far as I know your setup 'should' work but have you tried seperating CopyFile out into its own file under the copy_process folder? My guess is that since the copy_process folder exists, it is expecting all CopyProcess::* classes to be defined there instead of the copy_process.rb.
EDIT: You may consider opening another question, but the second half of your question is a different problem entirely.
You define methods in your module like so,
module X
def method_one
puts "hi"
end
end
Methods of this form are instance methods on the module, and they have very special restrictions. For instance, you can not access them from outside the module definition (I'm skeptical how these worked previously). Executing the above gives
> X::method_one
NoMethodError: undefined method `method_one' for X:Module
If you want to access these methods from other scopes you have a few options.
Use Class Methods
module X
def self.method_one
puts "hi"
end
end
X::hi #=> "hi"
Use Mixins
module X
module Helpers
def method_one
puts "hi"
end
end
end
class CopyFile
include X::Helpers
def some_method
method_one #=> "hi"
self.method_one #=> "hi"
end
end
I know this might be a dumb question. I'm trying to use this xml parser
http://nokogiri.rubyforge.org/nokogiri/Nokogiri.html
I've put the code below in a controller in a bringRSS method(?), and it works fine in IRB. But how do I get values for puts link.content into my views
def bringRSS
require 'nokogiri'
require 'open-uri'
# Get a Nokogiri::HTML:Document for the page we’re interested in...
doc = Nokogiri::HTML(open('http://www.google.com/search?q=tenderlove'))
# Do funky things with it using Nokogiri::XML::Node methods...
####
# Search for nodes by css
doc.css('h3.r a.l').each do |link|
puts link.content
end
####
# Search for nodes by xpath
doc.xpath('//h3/a[#class="l"]').each do |link|
puts link.content
end
####
# Or mix and match.
doc.search('h3.r a.l', '//h3/a[#class="l"]').each do |link|
puts link.content
end
end
Your method is a rails action ? If so, the "puts" method is inappropriate. You should define some global vars that'll be accessible in the view.
#css_content = Array.new
doc.css('h3.r a.l').each do |link|
#css_content << link.content
end
You define an #css_content array which contains every of your links.
And in your view you can use that var just like you usually use them in views.
The use of puts in a Rails action will throw an exception.
Instead just assign the data to an instance variable, like this:
#link_content = []
...
doc.css('h3.r a.l').each do |link|
#link_content << link.content
end
...
You can access it later in your views with the same name
Technically, you could write directly to your response, as it behaves more or less like the object you puts to in IRB. But as mentioned above, the Rails way of doing it is to use the controller to assign to instance vars and your view to render them.
As a benefit, you'll be able to use some really nice Ruby when assigning:
#links = doc.css('h3.r a.l').map{|link|link.content}
will map what each object's content method returns, just like you did above. And since Rails extends this by giving symbol objects a to_proc method, you could shorten it to
#links = doc.css('h3.r a.l').map(&:content)