Using ActiveResource to manipulate another application's Database - activeresource

I've come to seek your collective wisdom.
My goal, an overview:
In order to better manage computers for various clients, I'm attempting to extend Puppet's Dashboard. It's a Rails 2 application, and I'm trying to extend it with a Rails 3 application I'm writing. There are a few problems that make Dashboard less than perfect for my needs, but the solutions are simple. I'm going to focus on one, because I feel that the answer to this question will help me figure the rest out. I've been looking into solutions that don't alter the dashboard code at all, because I'm not the maintainer, and don't want to make a future mess.
I thought a lot about how to do this best. I thought about plugging directly into the database but I got cold feet after doing a little googling. It appears that setting up a second database connection isn't that difficult, the thing I don't like is altering another application's database while it's running. Please say something if I'm passing up a perfectly reasonable option, based on superstition.
There were a few other ideas, but the one that I started in on finally, and had marginal success with was accessing Dashboard's database via REST. It's built in, why not use it? Well, I was able to manipulate a couple of the tables, but not the one that I wanted to. So there are three tables to be aware of in this situation.
nodes (basically computers)
node_groups (the groups you can put computers in)
node_group_memberships (the join table that relates 1 and 2 to each other)
I can add and remove both nodes, and node_groups, but I want to be able to create a connection between the two as well. In order to create a new user, I have an ActiveResource model set up that looks like this:
class PuppetNode < ActiveResource::Base
self.site = "http://127.0.0.1:4000/"
self.element_name = "node"
attr_accessor :grouped
end
I'm then free to create new nodes, or grab info from the nodes table via the console. It might look something like this:
PuppetNode.create(:column_name => "and so on")
The same goes for node_groups, and I can even create a Rails 3 model that doesn't wig out for node_group_memberships, but I can't create anything in that table. I can see if I look at the Rails 2 node_group_membership controller (by the good folks over at Puppet Labs), that there is a create method
class NodeGroupMembershipsController < InheritedResources::Base
respond_to :json
before_filter :raise_if_enable_read_only_mode, :only => [:new, :edit, :create, :update, :destroy]
before_filter :standardize_post_data, :only => [:create]
def create
create! do |success, failure|
success.json { render :text => #node_group_membership.to_json, :content_type => 'application/json', :status => 201 }
failure.json { render :text => {}.to_json, :content_type => 'application/json', :status => 422 }
end
end
# we want: {node_group_membership => {node_id => <id>, node_group_id => <id>}
# allow and convert: {node_name => <name>, group_name => <name>}
def standardize_post_data
unless params[:node_group_membership]
params[:node_group_membership] = {}
node = Node.find_by_name(params[:node_name])
group = NodeGroup.find_by_name(params[:group_name])
params[:node_group_membership][:node_id] = (node && node.id)
params[:node_group_membership][:node_group_id] = (group && group.id)
end
end
end
But for whatever reason it chokes out every time I try to create an association with something like this
irb(main):005:0> PuppetNodeGroupMembership.create(:node_id => 20, :node_group_id => 5)
=> #<PuppetNodeGroupMembership:0x007fb3150af878 #attributes={"node_id"=>20, "node_group_id"=>5}, #prefix_options={}, #persisted=false, #remote_errors=#<ActiveResource::ResourceInvalid: Failed. Response code = 422. Response message = .>, #validation_context=nil, #errors=#<ActiveResource::Errors:0x007fb3150af4e0 #base=#<PuppetNodeGroupMembership:0x007fb3150af878 ...>, #messages={}>>
Any advice would be much appreciated, I've already put a solid 8 miserable hours into trying to figure it out. Thanks!

For better or for worse one thing that does work is connecting to a second database with the same Rails application. I'm leaving this marked as "An Answer" for now, but not "The Answer".
This is the resource that I found that helped me along:
http://www.rubynaut.net/articles/2008/05/31/how-to-access-multiple-database-in-rails.html

Related

Creating a Ruby API

I have been tasked with creating a Ruby API that retrieves youtube URL's. However, I am not sure of the proper way to create an 'API'... I did the following code below as a Sinatra server that serves up JSON, but what exactly would be the definition of an API and would this qualify as one? If this is not an API, how can I make in an API? Thanks in advance.
require 'open-uri'
require 'json'
require 'sinatra'
# get user input
puts "Please enter a search (seperate words by commas):"
search_input = gets.chomp
puts
puts "Performing search on YOUTUBE ... go to '/videos' API endpoint to see the results and use the output"
puts
# define query parameters
api_key = 'my_key_here'
search_url = 'https://www.googleapis.com/youtube/v3/search'
params = {
part: 'snippet',
q: search_input,
type: 'video',
videoCaption: 'closedCaption',
key: api_key
}
# use search_url and query parameters to construct a url, then open and parse the result
uri = URI.parse(search_url)
uri.query = URI.encode_www_form(params)
result = JSON.parse(open(uri).read)
# class to define attributes of each video and format into eventual json
class Video
attr_accessor :title, :description, :url
def initialize
#title = nil
#description = nil
#url = nil
end
def to_hash
{
'title' => #title,
'description' => #description,
'url' => #url
}
end
def to_json
self.to_hash.to_json
end
end
# create an array with top 3 search results
results_array = []
result["items"].take(3).each do |video|
#video = Video.new
#video.title = video["snippet"]["title"]
#video.description = video["snippet"]["description"]
#video.url = video["snippet"]["thumbnails"]["default"]["url"]
results_array << #video.to_json.gsub!(/\"/, '\'')
end
# define the API endpoint
get '/videos' do
results_array.to_json
end
An "API = Application Program Interface" is, simply, something that another program can reliably use to get a job done, without having to busy its little head about exactly how the job is done.
Perhaps the simplest thing to do now, if possible, is to go back to the person who "tasked" you with this task, and to ask him/her, "well, what do you have in mind?" The best API that you can design, in this case, will be the one that is most convenient for the people (who are writing the programs which ...) will actually have to use it. "Don't guess. Ask!"
A very common strategy for an API, in a language like Ruby, is to define a class which represents "this application's connection to this service." Anyone who wants to use the API does so by calling some function which will return a new instance of this class. Thereafter, the program uses this object to issue and handle requests.
The requests, also, are objects. To issue a request, you first ask the API-connection object to give you a new request-object. You then fill-out the request with whatever particulars, then tell the request object to "go!" At some point in the future, and by some appropriate means (such as a callback ...) the request-object informs you that it succeeded or that it failed.
"A whole lot of voodoo-magic might have taken place," between the request object and the connection object which spawned it, but the client does not have to care. And that, most of all, is the objective of any API. "It Just Works.™"
I think they want you to create a third-party library. Imagine you are schizophrenic for a while.
Joe wants to build a Sinatra application to list some YouTube videos, but he is lazy and he does not want to do the dirty work, he just wants to drop something in, give it some credentials, ask for urls and use them, finito.
Joe asks Bob to implement it for him and he gives him his requirements: "Bob, I need YouTube library. I need it to do:"
# Please note that I don't know how YouTube API works, just guessing.
client = YouTube.new(api_key: 'hola')
video_urls = client.videos # => ['https://...', 'https://...', ...]
And Bob says "OK." end spends a day in his interactive console.
So first, you should figure out how you are going to use your not-yet-existing lib, if you can – sometimes you just don't know yet.
Next, build that library based on the requirements, then drop it in your Sinatra app and you're done. Does that help?

ActiveRecord Won't Update

My simple Sqlite3 database is as follows:
CREATE TABLE balances(
balance zilch
);
My Ruby is as follows:
require('active_record')
ActiveRecord::Base.establish_connection(:database => "testbalance.db", :adapter => "sqlite3")
class Balance < ActiveRecord::Base
end
x = Balance.new
x.balance = 50
x.save
When I exit, and come back, and enter in the same Ruby again, at first, (before I runx.balance = 50) balance is nil. Why is this? Why isn't my DB saving?
If you enter the same code, then you're creating a new object again. No wonder its balance is nil.
To check that your object is saved, you can (for example) check Balance.count before and after record creation.
This is an old demo way of using Active Record and not very useful for production. It will get you started though. My code will make connections without sqlite3 gem required. I think that Active Record will include it if you use the :adapter hash entry. Of course you need it installed but it's not really needed in your code for Active Record. Just try it without that require to see. Then if you're still in doubt, un-install the gem just for fun. There are more Active Record namespaces and methods you should try especially ones that check to see if the database already exists. Then by pass the creation of one.
Here's some sample code from the book Metaprogramming Ruby.
#---
# Excerpted from "Metaprogramming Ruby",
# published by The Pragmatic Bookshelf.
# Copyrights apply to this code. It may not be used to create training material,
# courses, books, articles, and the like. Contact us if you are in doubt.
# We make no guarantees that this code is fit for any purpose.
# Visit http://www.pragmaticprogrammer.com/titles/ppmetr2 for more book information.
#---
# Create a new database each time
File.delete 'dbfile' if File.exist? 'dbfile'
require 'active_record'
ActiveRecord::Base.establish_connection :adapter => "sqlite3",
:database => "dbfile.sqlite3"
# Initialize the database schema
ActiveRecord::Base.connection.create_table :ducks do |t|
t.string :name
end
class Duck < ActiveRecord::Base
validate do
errors.add(:base, "Illegal duck name.") unless name[0] == 'D'
end
end
my_duck = Duck.new
my_duck.name = "Donald"
my_duck.valid? # => true
my_duck.save!
require_relative '../test/assertions'
assert my_duck.valid?
bad_duck = Duck.new(:name => "Ronald")
assert !bad_duck.valid?
duck_from_database = Duck.first
duck_from_database.name # => "Donald"
assert_equals "Donald", duck_from_database.name
duck_from_database.delete
File.delete 'dbfile' if File.exist? 'dbfile'
This code deletes the db file after usage and that's not very good persistence either. But you get the idea as it's just for testing assertions. You could try that to be sure as you change balances.
Do you want the rest of the code? https://pragprog.com/book/ppmetr/metaprogramming-ruby
Am I training you or am I the like? Moderators delete this if I'm wrong here please. I don't want to set a bad example.

Specifying the offset and limit in a Redmine REST API request with Ruby

I'm using the Ruby REST API for Redmine (here: http://www.redmine.org/projects/redmine/wiki/Rest_api_with_ruby). I need to be able to get all issues in a chunk of 100 at a time.
I know there is an options[:offset] and an options[:limit] that the method "api_offset_and_limit" is looking for.
How do I pass those options when I'm doing this? I tried putting them in the URL as GET options, but they didn't come through on the other end. The following gives me the first 25 issues, as I expect it to.
class Issue < ActiveResource::Base
self.site = 'http://redmine.server/'
self.user = 'foo'
self.password = 'bar'
end
# Retrieving issues
issues = Issue.find(:all)
I'm not familiar with the API, but the way you describe it, the following should work:
issues = Issue.find(:all, :params => {:offset => 0, :limit => 100})

How can I reduce redirects in my Rails 3 web app?

I'm new to Rails. I'm developing a store builder.
What I want
I want a root level url for each shop.
http://greatsite.com/my-shop-name
My Solution
shop_controller.rb
def show
if params[:url]
#shop_ref = params[:url]
#shop = Shop.where(:url => #shop_ref).first
else
#shop_ref = params[:id]
#shop = Shop.find(#shop_ref)
redirect_to "/" + #shop.url
return
end
if #shop.nil?
render 'show_invalid_shop', :object => #shop_ref and return
end
render 'show' => #shop
end
def create
#shop_url = (0...8).map{65.+(rand(25)).chr}.join.downcase
#shop = Shop.new(:url => #shop_url)
if #shop.save
redirect_to "/" + #shop.url
else
render :action => "new"
end
end
routes.rb
...
resources :shops
match ':url' => 'shops#show', :constraints => { :url => /[a-z|0-9]{4,30}/ }
...
The Problem
Crap Performance. (It's ugly as sin too, of course.)
Every time someone creates a new shop (which is one click from our home page), it creates a new shop and does a redirect. In New Relic, I see this is killing performance - a lot of time is spent in "Request Queuing".
Is there any neater and faster way of achieving what I want?
I'm not sure why the redirects would be causing such a headache, but:
Could you do something like:
Create the shop via an AJAX call.
On a successful create via AJAX render the show view, and return the html "string".
Replace the contents of the page with JS, and use pushstate to update the URL.
Might be useful to look at: http://pjax.heroku.com/
It's not exactly pretty, but if redirects are really that bad it might help?
I wouldn't recommend this, as it violates the REST principle...
But you could have create call/render the show action after it's done it's object creation (just like you do with "new" when it fails). That would eliminate the redirect but still show the same content as if it had.
There's a lot of reasons why you wouldn't want to do this. I'd look for performance improvements in other places first.

How can I persistently overwrite an attribute initialized by Rack::Builder?

I am trying to use OmniAuth to handle the OAuth flow for a small-ish Sinatra app. I can get 37signals Oauth to work perfectly, however I'm trying to create a strategy for Freshbooks Oauth as well.
Unfortunately Freshbooks require OAuth requests to go to a user specific subdomain. I'm acquiring the subdomain as an input and I then need to persistently use the customer specific site URL for all requests.
Here's what I've tried up to now. The problem is that the new site value doesn't persist past the first request.
There's to to be a simple way to achieve this but I'm stumped.
#Here's the setup -
def initialize(app, consumer_key, consumer_secret, subdomain='api')
super(app, :freshbooks, consumer_key, consumer_secret,
:site => "https://"+subdomain+".freshbooks.com",
:signature_method => 'PLAINTEXT',
:request_token_path => "/oauth/oauth_request.php",
:access_token_path => "/oauth/oauth_access.php",
:authorize_path => "/oauth/oauth_authorize.php"
)
end
def request_phase
#Here's the overwrite -
consumer.options[:site] = "https://"+request.env["rack.request.form_hash"]["subdomain"]+".freshbooks.com"
request_token = consumer.get_request_token(:oauth_callback => callback_url)
(session[:oauth]||={})[name.to_sym] = {:callback_confirmed => request_token.callback_confirmed?,
:request_token => request_token.token,
:request_secret => request_token.secret}
r = Rack::Response.new
r.redirect request_token.authorize_url
r.finish
end
Ok, here's a summary of what I did for anyone who comes across this via Google.
I didn't solve the problem in the way I asked it, instead I pushed the subdomain into the session and then I overwrite it whenever the site value needs to be used.
Here's the code:
#Monkeypatching to inject user subdomain
def request_phase
#Subdomain is expected to be submitted as <input name="subdomain">
session[:subdomain] = request.env["rack.request.form_hash"]["subdomain"]
consumer.options[:site] = "https://"+session[:subdomain]+".freshbooks.com"
super
end
#Monkeypatching to inject subdomain again
def callback_phase
consumer.options[:site] = "https://"+session[:subdomain]+".freshbooks.com"
super
end
Note that you still have to set something as the site when it's initialised, otherwise you will get errors due to OAuth not using SSL to make the requests.
If you want to see the actual code I'm using it's at: https://github.com/joeharris76/omniauth I'll push the fork up to the main project once I've battle tested this solution a bit more.

Resources