refactoring rails3 models with use with an external model API - ruby

How could I optimize (refactor) this non activerecord based model in rails3 that i have created.?
application.rb contains this:
CS = CloudServers::Connection.new(:username => '<hidden>', :api_key => '<hidden>')
cloudserver.rb (model) contains this:
class Cloudserver
# extend ActiveModel::Naming
attr_reader :id
attr_reader :name
attr_reader :image_id
attr_reader :flavor_id
attr_reader :status
attr_reader :progress
attr_reader :host_id
def initialize(id,name,image_id,flavor_id,status,progress,host_id)
#id = id
#name = name
#image_id = image_id
#flavor_id = flavor_id
#status = status
#progres = progress
#host_id = host_id
end
def self.all
server = CS.servers.map { |i|
new(i[:id],i[:name],i[:imageId],i[:flavorId],i[:status],i[:progress],i[:hostId])
# new(i)
}
end
def self.find(param)
all.detect { |l| l.id == param.to_i } || raise(ActiveRecord::RecordNotFound)
end
# def self.new
# server = CS.create_server(:name => "BOOYA", :imageId => 49, :flavorId => 2, :metadata => {'Luke' => 'Awesome'})
# end
end
FYI i am trying to build rails models for this api:
https://github.com/rackspace/ruby-cloudservers
should I bother or just have the controllers access the CS Object directly

Just for the sake of refactoring, here's a shorter way of writing the same class
class Cloudserver < Struct.new(:id, :name, :image_id, :flavor_id, :status, :progress, :host_id)
class << self
def all
server = CS.servers.map { |i|
new(i[:id],i[:name],i[:imageId],i[:flavorId],i[:status],i[:progress],i[:hostId])
}
end
def find
all.detect { |l| l.id == param.to_i } || raise(ActiveRecord::RecordNotFound)
end
end
end

Related

Copy text in html.slim using clipboard.js

I have a two-factor verification page, a secret key(Ciphertext) is displayed on it and I already have clipboard.js installed in my application.
I wonder how it is possible to create a button to copy that secret key?
= simple_form_for #google_auth, as: 'google_auth', url: verify_google_auth_path do |f|
h4 = t('.step-1')
p
span = t('.download-app')
span == t('.guide-link')
h4 = t('.step-2')
p: span = t('.scan-qr-code')
= f.input :uri do
= qr_tag(#google_auth.uri)
= f.input :otp_secret do
.input-group
= f.input_field :otp_secret, class: 'upcase', readonly: true
span.input-group-btn
a.btn.btn-default href='#{verify_google_auth_path(:app, refresh: true)}'
i.fa.fa-refresh
h4 = t('.step-3')
p: span = t('.enter-passcode')
= f.input :otp
hr.split
= f.button :wrapped, t('.submit'), cancel: settings_path
= content_for :guide do
ul.list-unstyled
li: a target='_blank' href='https://apps.apple.com/br/app/authy/id494168017'
i.fa.fa-apple
span = t('.ios')
li: a target='_blank' href='https://play.google.com/store/apps/details?id=com.authy.authy'
i.fa.fa-android
span = t('.android')
I tried to do like this, but it didn't work:
a.btn.btn-default data-clipboard-action='copy' data-clipboard-target=':otp_secret'
i.fa.fa-clipboard
In the example above it is copying only the pure otp_secret text.
spec\models\two_factor\app_spec.rb:
require 'spec_helper'
describe TwoFactor::App do
let(:member) { create :member }
let(:app) { member.app_two_factor }
describe "generate code" do
subject { app }
its(:otp_secret) { should_not be_blank }
end
describe '#refresh' do
context 'inactivated' do
it {
orig_otp_secret = app.otp_secret.dup
app.refresh!
expect(app.otp_secret).not_to eq(orig_otp_secret)
}
end
context 'activated' do
subject { create :two_factor_app, activated: true }
it {
orig_otp_secret = subject.otp_secret.dup
subject.refresh!
expect(subject.otp_secret).to eq(orig_otp_secret)
}
end
end
describe 'uniq validate' do
let(:member) { create :member }
it "reject duplicate creation" do
duplicate = TwoFactor.new app.attributes
expect(duplicate).not_to be_valid
end
end
describe 'self.fetch_by_type' do
it "return nil for wrong type" do
expect(TwoFactor.by_type(:foobar)).to be_nil
end
it "create new one by type" do
expect {
expect(app).not_to be_nil
}.to change(TwoFactor::App, :count).by(1)
end
it "retrieve exist one instead of creating" do
two_factor = member.app_two_factor
expect(member.app_two_factor).to eq(two_factor)
end
end
describe '#active!' do
subject { member.app_two_factor }
before { subject.active! }
its(:activated?) { should be_true }
end
describe '#deactive!' do
subject { create :two_factor_app, activated: true }
before { subject.deactive! }
its(:activated?) { should_not be_true }
end
describe '.activated' do
before { create :member, :app_two_factor_activated }
it "should has activated" do
expect(TwoFactor.activated?).to be_true
end
end
describe 'send_notification_mail' do
let(:mail) { ActionMailer::Base.deliveries.last }
describe "activated" do
before { app.active! }
it { expect(mail.subject).to match('Google authenticator activated') }
end
describe "deactived" do
let(:member) { create :member, :app_two_factor_activated }
before { app.deactive! }
it { expect(mail.subject).to match('Google authenticator deactivated') }
end
end
end
app.rb:
class TwoFactor::App < ::TwoFactor
def verify?
return false if otp_secret.blank?
rotp = ROTP::TOTP.new(otp_secret)
if rotp.verify(otp)
touch(:last_verify_at)
true
else
errors.add :otp, :invalid
false
end
end
def uri
totp = ROTP::TOTP.new(otp_secret)
totp.provisioning_uri(member.email) + "&issuer=#{ENV['URL_HOST']}"
end
def now
ROTP::TOTP.new(otp_secret).now
end
def refresh!
return if activated?
super
end
private
def gen_code
self.otp_secret = ROTP::Base32.random_base32
self.refreshed_at = Time.new
end
def send_notification
return if not self.activated_changed?
if self.activated
MemberMailer.google_auth_activated(member.id).deliver
else
MemberMailer.google_auth_deactivated(member.id).deliver
end
end
end
EDIT:
app\models\two_factor.rb:
class TwoFactor < ActiveRecord::Base
belongs_to :member
before_validation :gen_code, on: :create
after_update :send_notification
validates_presence_of :member, :otp_secret, :refreshed_at
attr_accessor :otp
SUBCLASS = ['app', 'sms', 'email', 'wechat']
validates_uniqueness_of :type, scope: :member_id
scope :activated, -> { where(activated: true) }
scope :require_signin, -> { where(require_signin: 1) }
class << self
def by_type(type)
return if not SUBCLASS.include?(type.to_s)
klass = "two_factor/#{type}".camelize.constantize
klass.find_or_create_by(type: klass.name)
end
def activated?
activated.any?
end
def require_signin?
require_signin.any?
end
end
def verify?
msg = "#{self.class.name}#verify? is not implemented."
raise NotImplementedError.new(msg)
end
def expired?
Time.now >= 30.minutes.since(refreshed_at)
end
def refresh!
gen_code
save
end
def active!
update activated: true, last_verify_at: Time.now
end
def set_require_signin
update require_signin: 1
end
def reset_require_signin
update require_signin: nil
end
def deactive!
update activated: false, require_signin: nil
end
private
def gen_code
msg = "#{self.class.name}#gen_code is not implemented."
raise NotImplementedError.new(msg)
end
def send_notification
msg = "#{self.class.name}#send_notification is not implemented."
raise NotImplementedError.new(msg)
end
end
What it seems you're trying to do is just to copy the value of an input field(which has been populated by other code you have) to the system clipboard. You need to use javascript to do this, if you have jquery this should work.
For your slim you need an id to target it
a.btn.btn-default id= "copy"
i.fa.fa-clipboard
Try to add an id to the input element you want to copy from
= f.input_field :otp_secret, class: 'upcase', id: "secret", readonly: true
Now try to change this and see if works.
a.btn.btn-default data-clipboard-action='copy' data-clipboard-target='secret'
i.fa.fa-clipboard
Also somewhere in your javascript you'll need to target the clip event with something like this:
new ClipboardJS('#secret');
See example here https://jsfiddle.net/ec3ywrzd/
Then you'll need this javascript to load in your html. But you'll need to be able to target the cipher field, in this example I'm using id="secret". I'm not sure if the OTP code you have generates it's own ID or now, so you may need to inspect your dom to figure out how to target it to add an ID. You may try adding an ID here:
= f.input_field :otp_secret, class: 'upcase', id: "secret", readonly: true
Otherwise you'll have to use other query selectors to target it.
But you may not need clipboardjs at all.
Here's a basic example on jsfiddle to test it you can just add any string to the input field. You'll need to add this to a JS file which will be loaded by your view layout, i.e. application.js
$(document).ready(function() {
$('#copy').click(function(){
$('#secret').select();
document.execCommand('copy');
alert("copied!");
})
})
You may also see answers to this question
I managed to solve based on suggestions from our friend #lacostenycoder.
There was only a need to change even in the show.html.slim file, looking like this:
= simple_form_for #google_auth, as: 'google_auth', url: verify_google_auth_path do |f|
h4 = t('.step-1')
p
span = t('.download-app')
span == t('.guide-link')
h4 = t('.step-2')
p: span = t('.scan-qr-code')
= f.input :uri do
= qr_tag(#google_auth.uri)
= f.input :otp_secret do
.input-group
.form-control.form-control-static = #google_auth.otp_secret
.input-group
a.btn.btn-default href="javascript:void(0)" data-clipboard-text = #google_auth.otp_secret
i.fa.fa-clipboard
a.btn.btn-default href='#{verify_google_auth_path(:app, refresh: true)}'
i.fa.fa-refresh
h4 = t('.step-3')
p: span = t('.enter-passcode')
= f.input :otp
hr.split
= f.button :wrapped, t('.submit'), cancel: settings_path
= content_for :guide do
ul.list-unstyled
li: a target='_blank' href='https://apps.apple.com/br/app/authy/id494168017'
i.fa.fa-apple
span = t('.ios')
li: a target='_blank' href='https://play.google.com/store/apps/details?id=com.authy.authy'
i.fa.fa-android
span = t('.android')

Using find_or_create_by! in before_action filter

I have a weird behaviour when using User.find_or_create_by! in before_action filter as follows:
class ApplicationController < ActionController::API
before_action :authorize_request
attr_reader :current_user
private
def authorize_request
#current_user = (AuthorizeApiRequest.new(request.headers).call)[:user]
end
end
Then in AuthorizeApiRequest I'm checking for existence or creating a new User by name:
class AuthorizeApiRequest
def initialize(headers = {})
#headers = headers
end
def call
{
user: user
}
end
def user
if decoded_auth_token && decoded_auth_token[:sub]
#user ||= User.find_or_create_by!(username: decoded_auth_token[:sub])
Rails.logger.silence do
#user.update_column(:token, http_auth_header)
end
#user
end
rescue ActiveRecord::RecordInvalid => e
raise(
ExceptionHandler::InvalidToken,
("#{Message.invalid_token} #{e.message}")
)
end
end
Example of UsersController:
class UsersController < ApplicationController
def me
if user_info_service.call
json_response current_user, :ok, include: 'shop'
else
raise AuthenticationError
end
end
private
def user_info_service_class
#user_info_service_class ||= ServiceProvider.get(:user_info_service)
end
def user_info_service
#user_info_service ||= user_info_service_class.new(user: current_user)
end
end
What is weird is that sometimes the User is created twice with the same username, sometimes not.
I'm using Ember JS in the front and another call is made to shops right after the authentication with JWT. All the routes are protected. I have the impression that calling current_user is not always in the same thread or sth like that and it results in having 2 identical users:
- the first one with just a username attribute set
- another one with all the others User attributes.
Here is the User model:
class User < ApplicationRecord
validates :username, presence: true, uniqueness: { case_sensitive: false }, on: :create
validates :shop_identifier, numericality: { only_integer: true, greater_than: 0 }, on: :update
validates :first_name, presence: true, on: :update
validates :last_name, presence: true, uniqueness: { case_sensitive: false, scope: :first_name }, on: :update
before_update do |user|
user.first_name = first_name.strip.capitalize
user.last_name = last_name.strip.upcase
end
Any ideas ? Thank you

undefined method `activation_digest=' for #<User:0x007fe3810ceba0> Michael Hartl's book

I am working through Michael Hartl's Rails book and I am about halfway through chapter 10-working on account activation.
I had everything working with the mailers but then when I tried to add a new user, I got the following error message: "undefined method `activation_digest=' for #"
I have been trying to follow along in the book the best that I can. I have my users_controller.rb here:
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update]
before_action :correct_user, only: [:edit, :update]
def new
#user = User.new
end
def index
#users = User.paginate(page: params[:page], :per_page => 10)
end
def show
#user = User.find(params[:id])
end
def create
#user = User.new(user_params)
if #user.save
#user.send_activation_email
flash[:info] = "Please check your email to activate your account."
redirect_to root_url
else
render 'new'
end
end
def update
#user = User.find(params[:id])
if #user.update_attributes(user_params)
else
render 'edit'
end
end
def edit
#user = User.find(params[:id])
end
#confirms if a user is logged in
def logged_in_user
unless logged_in?
store_location
flash[:danger] = "Please Log In."
redirect_to login_url
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end
Here is my Model/user.rb:
class User < ActiveRecord::Base
attr_accessor :remember_token, :activation_token
before_save :downcase_email
before_create :create_activation_digest
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, length: { minimum: 6 }
# Returns the hash digest of the given string.
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
#Returns a random token
def User.new_token
SecureRandom.urlsafe_base64
end
#Remembers a user in the database for use in persistent sessions
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
#Returns true if the given token matches the digest
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
#forgets a user
def forget
update_attribute(:remember_digest, nil)
end
private
# Converts email to all lower-case.
def downcase_email
self.email = email.downcase
end
# Creates and assigns the activation token and digest.
def create_activation_digest
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
end
end
The routes I have this:
root 'static_pages#home'
get 'sessions/new'
get 'users/new'
get 'help' => 'static_pages#help'
get 'about' => 'static_pages#about'
get 'contact' => 'static_pages#contact'
get 'signup' => 'users#new'
get 'login' => 'sessions#new'
post 'login' => 'sessions#create'
delete 'logout' => 'sessions#destroy'
resources :users
resources :account_activations, only: [:edit]
Please let me know if anything more is needed to be seen. I do have my App up on Github under the name sample_app, my username is ravenusmc.
Looking at your project on Github, your User model doesn't have an activation_token or activation_digest column, nor does the model define them as attributes.
Your User model is trying to write to these columns in the User#create_activation_digest function which is most likely causing the issue.
You'll need to write a migration to add those columns to your User model or add them is attributes (ie attr_accessor) if they are not meant to be persisted.

Rails 3.2.9.Testing observer with RSpec(trouble with should_receive)

I have such problem. My test checks whether the Observer called, but does not execute it.
My files:
todo_observer.rb:
class TodoObserver < ActiveRecord::Observer
def after_create(todo)
todo.add_log('creating')
end
end
todo.rb:
class Todo < ActiveRecord::Base
attr_accessible :content, :done, :order
validates :content, :presence => true,
:length => {:minimum => 2}
def add_log(event)
Logdata.start_logging(self.content, event)
end
end
logdata.rb
class Logdata < ActiveRecord::Base
attr_accessible :modification, :event
def self.start_logging(content, event)
Logdata.create!(:modification => content, :event => event)
end
end
todo_observer_spec.rb:
require 'spec_helper'
describe TodoObserver do
before(:each) do
#attr = { :modification => "Example", :event => 'Event' }
#attr_todo = { :content => "Example", :done => :false }
end
describe 'after_create' do
it "should create log about creating task" do
count_log = Logdata.all.size
todo = Todo.new(#attr_todo)
todo.should_receive(:add_log).with('creating')
todo.save!
(Logdata.all.size).should eq(count_log + 1)
end
end
end
When I run test I get such error
Failure/Error: (Logdata.all.size).should eq(count_log + 1)
expected: 1
got: 0
Its mean, that observer called,but doesn't create instance of Logdata. When I comment string(check the call)
todo.should_receive(:add_log).with('creating')
My tests were successful.And accordingly its success when I comment string (Logdata.all.size).should eq(count_log + 1)and uncomment previous string.
How does the function should_receive to create an instance of the class Logdata?
should_receive prevents the actual method from being called.
You should create two separate tests. One to check that the log is added to the todo, and one to check that the log is created.
describe 'after_create' do
it "should add a log to the todo" do
todo = Todo.new(#attr_todo)
todo.should_receive(:add_log).with('creating')
todo.save!
end
it "should create a new logdata" do
todo = Todo.new(#attr_todo)
expect {
todo.save!
}.to change {Logdata.count}.by(1)
end
end

How to spec a file download

I am working on a library that needs to be able to download plugin files from a remote API using RestClient. The library first grabs a list of plugins, and then downloads each plugin as a raw file, saving each inside a plugins directory.
Here is what I have thus far but it is failing me:
require 'yaml'
module Monitaur
class Client
attr_accessor :logger, :client_key, :server_url, :config, :raw_config,
:plugin_manifest
def initialize
load_config
#plugin_manifest ||= []
end
def run
get_plugin_manifest
sync_plugins
end
def get_plugin_manifest
res = RestClient.get("#{server_url}/nodes/#{client_key}/plugins")
#plugin_manifest = JSON.parse(res)
end
def sync_plugins
#plugin_manifest.each do |plugin|
res = RestClient.get("#{server_url}/plugins/#{plugin['name']}")
File.open(File.join(Monitaur.plugin_dir, "#{plugin['name']}.rb"), "w+") do |file|
file.write res.body
end
end
end
def load_config
if File.exist?(Monitaur.config_file_path) && File.readable?(Monitaur.config_file_path)
#raw_config = YAML.load_file(Monitaur.config_file_path)
else
raise IOError, "Cannot open or read #{Monitaur.config_file_path}"
end
#server_url = raw_config['server_url']
#client_key = raw_config['client_key']
end
end
end
And the client_spec.rb
require 'spec_helper'
module Monitaur
describe Client do
let(:server_url) { "http://api.monitaurapp.com" }
let(:client_key) { "asdf1234" }
describe "#load_config" do
let(:client) { Monitaur::Client.new }
before do
File.open(Monitaur.config_file_path, "w") do |file|
file.puts "server_url: http://api.monitaurapp.com"
file.puts "client_key: asdf1234"
end
end
it "loads up the configuration file" do
client.load_config
client.server_url.should == "http://api.monitaurapp.com"
client.client_key.should == "asdf1234"
end
end
describe "#get_plugin_manifest" do
let(:client) { Monitaur::Client.new }
before do
stub_get_plugin_manifest
end
it "retrieves a plugins manifest from the server" do
client.get_plugin_manifest
client.plugin_manifest.should == plugin_manifest_response
end
end
describe "#sync_plugins" do
let(:client) { Monitaur::Client.new }
let(:foo_plugin) { mock('foo_plugin') }
let(:bar_plugin) { mock('bar_plugin') }
before do
FileUtils.mkdir("/tmp")
File.open("/tmp/foo_plugin.rb", "w+") do |file|
file.write %|
class FooPlugin < Monitaur::Plugin
name "foo_plugin"
desc "A test plugin to determine whether plugin sync works"
def run
{ :foo => 'foo' }
end
end
|
end
File.open("/tmp/bar_plugin.rb", "w+") do |file|
file.write %|
class BarPlugin < Monitaur::Plugin
name "bar_plugin"
desc "A test plugin to determine whether plugin sync works"
def run
{ :bar => 'bar' }
end
end
|
end
Monitaur.install
stub_get_plugin_manifest
stub_sync_plugins
client.get_plugin_manifest
end
it "downloads plugins to the cache directory" do
File.should_receive(:open).
with(File.join(Monitaur.plugin_dir, "foo_plugin.rb"), "w+")
and_yield(foo_plugin)
client.sync_plugins
File.exist?("/home/user/.monitaur/cache/plugins/foo_plugin.rb").should be_true
File.exist?("/home/user/.monitaur/cache/plugins/bar_plugin.rb").should be_true
end
end
end
end
def stub_get_plugin_manifest
stub_request(:get, "#{server_url}/nodes/#{client_key}/plugins").
to_return(
:status => 200,
:body => %Q{
[
{
"name": "foo_plugin",
"checksum": "qwer5678"
},
{
"name": "bar_plugin",
"checksum": "hjkl4321"
}
]
}
)
end
def plugin_manifest_response
[
{
"name" => "foo_plugin",
"checksum" => "qwer5678"
},
{
"name" => "bar_plugin",
"checksum" => "hjkl4321"
}
]
end
def stub_sync_plugins
stub_request(:get, "#{server_url}/plugins/foo_plugin").
to_return(:body => File.open('/tmp/foo_plugin.rb').read)
stub_request(:get, "#{server_url}/plugins/bar_plugin").
to_return(:body => File.open('/tmp/bar_plugin.rb').read)
end
How can I test the download process?
I use FakeWeb for this purpose, as there's really no need for your spec to fail if the other site is down or something. See "Replaying a recorded response" in the docs. What we do is curl the page, save it somewhere as a fixture and replay that in the specs.

Resources