I am trying to achieve this feature:
Sent texts out to new authors.
It enables admin users to
when authors published their first article in a specified timeframe, Compose and send out emails to authors
Download a CSV file with a list of recipients.
I want to refactor the controller code in efficient way
Models
class Email < ActiveRecord::Base
end
class User < ActiveRecord::Base
has_many :articles
scope :recent, -> { order("created_at DESC") }
end
class Article < ActiveRecord::Base
end
# Controller
class EmailsController < ApplicationController
attr_accessor :email
def new
#email = Email.new
if params[:date_from].present? && params[:date_to].present?
fetch_users
end
end
def create
#email = Email.new(email_params)
users = fetch_users
recipients = []
users.each do |user|
recipients << user.email
end
if #email.save && send_mail_to_user(recipients)
redirect_to emails_path, notice: "Emails Sent Successfully!"
end
end
def download_csv
users = fetch_users
send_data to_csv(users), filename: "users-article-#{Time.zone.today}.csv"
end
private
def to_csv(data)
inputs = %w{id email name}
CSV.generate(headers: true) do |csv|
csv << attributes
data.each do |user|
csv << inputs.map { |attr| user.send(attr) }
end
end
end
def send_mail_to_user(users)
users.each do |r|
UserMailer.bulk_email(r).deliver_later
end
end
def fetch_users
#users = User.recent.joins(:articles).group("articles.user_id").where("`published_article_count` = 1").
where("published_at Between ? AND ?", Date.parse(params[:date_from]), Date.parse(params[:date_to])).page(params[:page])
end
def email_params
params.permit(:body, :date_from, :date_to)
end
end
Related
I want to "reinitialize" my User class, so i have a fresh state in each of my RSpec examples. I tried calling remove_const:
before(:each) do
Object.send(:remove_const, 'User')
load 'user.rb'
end
describe 'initializer' do
it 'creates an user' do
user = User.new("jules", "jules#gg.k")
expect(user.class).to eq(User)
end
it 'saves #email as instance variable' do
email = "jules#gg.com"
user = User.new("jules", email)
expect(user.email).to eq(email)
end
# ...
end
but it returns:
NameError: constant Object::User not defined
My User class looks like this:
class User
attr_accessor :name, :email
##user_list = []
##user_count = 0
def self.user_count
##user_count
end
def self.all
##user_list
end
def initialize(name, email)
#email = email
#name = name
##user_count += 1
##user_list << self
end
end
I see two options:
only remove the constant if it is actually defined:
if Object.const_defined?(:User)
Object.send(:remove_const, :User)
load 'user.rb' # <- you may have to adjust the path
end
add a class method to User to clear your in-memory user "database":
class User
def self.clear
##user_list = []
##user_count = 0
end
end
and call that instead:
before(:each) do
User.clear
end
I'd go with option #2.
require_relative '../lib/user'
describe User do
before(:each) do
Object.send(:remove_const, 'User')
load 'user'
end
describe 'initializer' do
it 'creates an user' do
user = User.new("jules", "jules#gg.k")
expect(user.class).to eq(User)
end
it 'saves #email as instance variable' do
email = "jules#gg.com"
user = User.new("jules", email)
expect(user.email).to eq(email)
end
it 'adds one to the ##user_count global variable' do
count = User.user_count
user = User.new("jules", "email#email.email")
expect(User.user_count).to eq(count+1)
end
end
describe 'classe method' do
describe 'all' do
it 'should return a list of names of all users' do
user1 = User.new("jacques", "jacky#chan.fr")
user2 = User.new("phil", "ph#il.ipe")
expect(User.all.map! {|k| k.name}).to eq(["jacques", "phil"])
end
end
describe 'find_by_email' do
it 'should return return a name if user exists and nil otherwise' do
user1 = User.new("jacques", "jacky#chan.fr")
user2 = User.new("phil", "ph#il.ipe")
expect(User.find_by_email("jacky#chan.fr")).to eq("jacques")
end
end
end
end
How to refactor #filtered method?
In Hanami there is no way to make a chain of queries (filters) in ActiveRecord-style. I would like to get a methods like ActiveRecord filters.
Now: documents.filtered(genre: 'news', min_published_at: from, max_published_at: to, skip: 30)
What I want: documents.with_genre('news').published_between(from, to).skip(30)
class DocumentRepository < Hanami::Repository
GENRES = DbSchema.current_schema.enum(:document_genre).values.map(&:to_s)
DOCUMENTS_PER_PAGE = 30
associations do
has_many :boxes
has_many :urls
end
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/AbcSize
def filtered(params = {})
result = ordered.limit(DOCUMENTS_PER_PAGE)
result = result.where(genre: params[:genre]) if params.key?(:genre)
if params.key?(:min_created_at) && params.key?(:max_created_at)
date_range = params[:min_created_at]..params[:max_created_at]
result = result.where(created_at: date_range)
end
if params.key?(:min_published_at) && params.key?(:max_published_at)
date_range = params[:min_published_at]..params[:max_published_at]
result = result.where(published_at: date_range)
end
result = result.offset(params[:skip]) if params.key?(:skip)
result
end
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/AbcSize
def ordered
documents.order { created_at.desc }
end
end
Something along these lines might work, but not sure how chaining these will poorly effect performance or results, but you can try it and it may lead you to the answer you want
UPDATED
If you really want chaining this is close to what you want.
class DocumentRepository < Hanami::Repository
GENRES = DbSchema.current_schema.enum(:document_genre).values.map(&:to_s)
DOCUMENTS_PER_PAGE = 30
associations do
has_many :boxes
has_many :urls
end
attr_accessor :data
def initialize
#data = []
super
end
def data
#data.flatten!.uniq!
end
def with_genre(key)
#data << documents.where(genre: key)
self
end
def published_between(arr)
from, to = arr
#data << documents.where(created_at: [from..to])
self
end
def skip(num)
#data << documents.offset(num)
self
end
end
Call it like this assuming this is an instance variable of DocumentRepository
document_repository.with_genre('news')
.published_between([from, to])
.skip(30)
.data
By returning self in each instance method you're able to chain the calls on the instance.
Original answer
This way works but uses similar syntax in your current call.
class DocumentRepository < Hanami::Repository
GENRES = DbSchema.current_schema.enum(:document_genre).values.map(&:to_s)
DOCUMENTS_PER_PAGE = 30
associations do
has_many :boxes
has_many :urls
end
def hack_where(opts={})
data = []
opts.each do |i|
data << self.send(i[0],i[1]).call
end
data.flatten!.uniq!
end
def with_genre(key)
lambda { |key| documents.where(genre: key) }
end
def published_between(arr)
from = arr[0]
to = arr[1]
lambda { |from, to| documents.where(created_at: [from..to]) }
end
def skip(num)
lambda { documents.offset(num) }
end
end
You can call it like:
hack_where({with_genre: 'news', published_between: [from,to], skip: 30})
Introduce query object:
class FilterDocuments
DOCUMENTS_PER_PAGE = 30
def initialize(documents)
#documents = documents
end
def filter(params = {})
result = apply_ordering(documents)
result = apply_limit_and_offset(result, params)
result = filter_by_genre(result, params)
result = filter_by_created_at(result, params)
result = filter_by_published_at(result, params)
result
end
private
attr_reader :documents
def apply_ordering(documents)
documents.order { created_at.desc }
end
def apply_limit_and_offset(documents, params)
if params.key?(:skip)
documents.offset(params[:skip])
else
documents
end.limit(DOCUMENTS_PER_PAGE)
end
def filter_by_genre(documents, params)
if params.key?(:genre)
documents.where(genre: params[:genre])
else
documents
end
end
def filter_by_created_at(documents, params)
if params.key?(:min_created_at) && params.key?(:max_created_at)
range = params[:min_created_at]..params[:max_created_at]
documents.where(created_at: range)
else
documents
end
end
def filter_by_published_at(documents, params)
if params.key?(:min_published_at) && params.key?(:max_published_at)
range = params[:min_published_at]..params[:max_published_at]
documents.where(published_at: range)
else
documents
end
end
end
How to use:
def query
FilterDocuments.new(DocumentRepository.new.documents)
end
filtered_documents = query.filter(params)
I have run into confusion tying together a test double and stubbing it. My question is - what is the most appropriate way to test the confirm_purchase_order and create_order methods in class PurchaseOrder?
I have included the relevant code following code:
class PurchaseOrder
attr_reader :customer, :products
def initialize(customer)
#products = {}
#customer = customer
end
....some other methods
def add_product(product, quantity = 1)
#products[product] = (#products[product] ? #products[product] + quantity : quantity )
puts "You haved added #{quantity} #{product.title}'s to your purchase order"
end
def confirm_purchase_order
purchase_order_total
raise "Your PO appears to be empty! Add some products and try again." unless self.total.to_f.round(2) > 0
create_order
create_invoice
return "We have generated an Invoice and created an order."
end
def create_order
order = Order.new(customer)
order.products = #products.clone
end
def create_invoice
invoice = Invoice.new(customer)
invoice.products = #products.clone
end
end
class Order
attr_reader :customer
attr_accessor :status, :total, :products
def initialize(customer)
#products = {}
#status = :pending
#customer = customer
end
class Customer
attr_reader :name, :type
def initialize(name, type)
#name = name.to_s
#type = type.to_sym
end
class Invoice
attr_reader :customer, :products
attr_accessor :total
def initialize(customer, products)
#products = {}
#customer = customer
#payment_recieved = false
end
end
I want to test the confirm_purchase_order method as well as the create_order method in class PurchaseOrder. My approach so far:
I need some object doubles and an actual PurchaseOrder object
describe PurchaseOrder do
let(:product) { double :product, title: "guitar", price: 5 }
let(:order) { instance_double(Order) }
let(:customer) { double :customer, name: "Bob", type: :company }
let(:products) { {:product => 1} }
let(:purchase_order) { PurchaseOrder.new(customer) }
describe "#create_order" do
it "returns an order" do
expect(Order).to receive(:new).with(customer).and_return(order)
allow(order).to receive(products).and_return(???products??!)
purchase_order.add_product(product, 1)
purchase_order.create_order
expect(order.products).to eq (products)
end
end
end
I have also looked at the use of:
# order.stub(:products).and_return(products_hash)
# allow_any_instance_of(Order).to receive(:products) { products_hash }
# order.should_receive(:products).and_return(products_hash)
To setup the order double to return a products hash when order.products is called, but these all feel like they are 'rigging' the test too much. What is the most appropriate way to test the confirm_purchase_order and create_order methods in class PurchaseOrder?
It seems to me that perhaps you're giving PurchaseOrder too much responsibility. It now has intimate knowledge about Order and Invoice.
I'd perhaps test the current implementation like this:
it "returns an order with the same products" do
expect_any_instance_of(Order).to receive(:products=).with(products: 1)
purchase_order.add_product(product, 1)
expect(purchase_order.create_order).to be_a(Order)
end
But maybe it could make sense to decouple PurchaseOrder from Order and Invoice a little bit and do something like this:
class Invoice
def self.from_purchase_order(purchase_order)
new(purchase_order.customer, purchase_order.products.clone)
end
end
class Order
def self.from_purchase_order(purchase_order)
new.tap(purchase_order.customer) do |invoice|
invoice.products = purchase_order.products.clone
end
end
end
class PurchaseOrder
# ...
def create_order
Order.from_purchase_order(self)
end
def create_invoice
Invoice.from_purchase_order(self)
end
end
describe PurchaseOrder do
let(:customer) { double('a customer')}
let(:purchase_order) { PurchaseOrder.new(customer) }
describe "#create_order" do
expect(Order).to receive(:from_purchase_order).with(purchase_order)
purchase_order.create_order
end
describe "#create_invoice" do
expect(Order).to receive(:from_purchase_order).with(purchase_order)
purchase_order.create_order
end
end
describe Order do
describe '.from_purchase_order' do
# test this
end
end
describe Order do
describe '.from_purchase_order' do
# test this
end
end
This way you let the Order and Invoice classes know how to build themselves from a PurchaseOrder. You can test these class methods separately. The tests for create_order and create_invoice become simpler.
Some other things I thought of:
For products, try using a Hash with a default proc:
#products = Hash.new { |hash, unknown_key| hash[unknown_key] = 0 }
This way, you can always safely do #products[product] += 1.
I'm a new Rubyist and am wondering how I can access the ingredients class from individual cookies? As we all know, cookies are made of different ingredients. How can I specify default ingredients for individual cookies without setting default values? Even if I had default values, how would I update those to reflect the most current "recipe"? Please and thanks!
#Cookie Factory
module CookieFactory
def self.create(args)
cookie_batch = []
args.each do |cookie|
cookie_batch << PeanutButter.new if cookie == "peanut butter"
cookie_batch << ChocholateChip.new if cookie == "chocolate chip"
cookie_batch << Sugar.new if cookie == "sugar"
end
return cookie_batch
end
end
#Classes/Subclasses
class Ingredients
attr_reader
def initialize(contents = {})
# contents = defaults.merge(contents)
#sugar = contents.fetch(:sugar, "1.5 cups")
#salt = contents.fetch(:salt, "1 teaspoon")
#gluten = contents.fetch(:gluten, "0")
#cinnamon = contents.fetch(:cinnamon, "0.5 teaspoon")
end
end
class Cookie
attr_reader :status, :ingredients
def initialize(ingredients = {})
#ingredients = ingredients
#status = :doughy
super()
end
def bake!
#status = :baked
end
end
class PeanutButter < Cookie
attr_reader :peanut_count
def initialize
#peanut_count = 100
super()
end
def defaults
{
:peanut_shells => 5
}
end
end
class Sugar < Cookie
attr_reader :sugar
def initialize
#sugar = "1_cup"
super()
end
end
class ChocholateChip < Cookie
attr_reader :choc_chip_count
def initialize
#choc_chip_count = 200
super()
end
end
You can use Hash#merge to acheive this behavior:
class PeanutButter < Cookie
attr_reader :peanut_count
def initialize(ingredients)
#peanut_count = 100
super(ingredients.merge(defaults))
end
def defaults
{
:peanut_shells => 5
}
end
end
#!/usr/bin/env ruby
# this is the data I have
#data = {
:student => {
:id => '123477',
:first_name => 'Lazlo',
:last_name =>'Fortunatus',
:email=>'Lazlo#fortunatus.org'
},
:contact_info => {
:telephone=>'1 415 222-2222',
:address => '123 Main St',
:city =>'Beverly Hills',
:state=>'California',
:zip_code=>90210,
:social_security_number =>'111-11-1111'
}
}
class Student
# not fully implemented - this is what I need help on.
def get_id_original
# I need this to return the value #data[:student][:id]
end
def get_city_original
# I need this to return the value #data[:contact_info][:city]
end
end
s = Student.new
# this is the original method
# how can I access the #data variable here I tried #data[:student][:id] doesnt work
# I realize that data is outside of the scope of this method. However, is there any way!
s.get_id_original
# My goal is to have a singleton method that acts exactly like get_id_original,
# but get_id_original doesn't work.
def s.id
get_id_original
end
It can be done!
It didn't at first work because #data is an instance attribute of the top level object, so even though Student is derived from Object the attribute isn't in the new instance.
But you can pass self into s.id, and so then the only thing you need to add is an accessor for the data attribute.
However, that's slightly tricky because attr_reader et al are private class methods so you can't use them directly, and you can't (because it's private) just say self.class.attr_reader, you have to open up Object and do it...with these changes your program works...
#data = { :student => { :id => '123477'} }
class Student
end
s = Student.new
def s.id o
o.data[:student][:id]
#how can I access the #data variable here I tried #data[:student][:id] doesnt work
#I realize that data is outside of the scope of this method. However, is there any way!
end
class Object
attr_reader :data
end
puts s.id self
First off, your id method actually has to go into the class.
You could try something like this:
#data = { :student => { :id => '123477'} }
class Student
attr_accessor :id
def initialize(student)
self.id = student[:id]
end
end
s = Student.new(#data[:student])
puts s.id
#!/usr/bin/ruby
#data = { :student => { :id => '123477', :first_name => 'Lazlo', :last_name =>'Fortunatus', :email=>'Lazlo#fortunatus.org' }, :contact_info => { :telephone=>'1 415 222-2222', :address => '123 Main St', :city =>'Beverly Hills', :state=>'California', :zip_code=>90210, :social_security_number =>'111-11-1111' } }
class Student
def initialize( data )
#data = data
end
def get_id_override
#data[:student][:id]
end
def get_first_name_override
#data[:student][:first_name]
end
def get_last_name_override
#data[:student][:last_name]
end
def get_email_override
#data[:student][:email]
end
def get_telephone_override
#data[:contact_info][:telephone]
end
def get_city_override
#data[:contact_info][:city]
end
def get_state_override
#data[:contact_info][:state]
end
def get_zip_code_override
#data[:contact_info][:zip_code]
end
def get_social_security_number_override
#data[:contact_info][:social_security_number]
end
end
s = Student.new(#data)
def s.id
get_id_override
end
def s.first_name
get_first_name_override
end
def s.last_name
get_last_name_override
end
def s.email
get_email_override
end
def s.contact_info
get_telephone_override
end
def s.city
get_city_override
end
def s.state
get_state_override
end
def s.zipcode
get_zip_code_override
end
def s.ssn
get_social_security_number_override
end
puts s.id
puts s.first_name
puts s.last_name
puts s.email
puts s.contact_info
puts s.city
puts s.state
puts s.zipcode
puts s.ssn
Here is the answer after some work. Anyone has a better answer than mine let me know.
You really should be passing in the data object so it s has its own reference to it.
#data = { :student => { :id => '123477'} }
class Student
attr_accessor :data
def initialize(data)
#data = data
end
end
s = Student.new(#data)
# or you can use s.data = #data
def s.id
#data[:student][:id]
end
puts s.id
A word of caution. Any modifications to #data in the outermost scope will be reflected in s, because both #data variables point to the same object in memory.
But what if you don't want to modify the Student class? You can just add the accessor to s:
#data = { :student => { :id => '123477'} }
class Student
end
s = Student.new
class << s
attr_accessor :data
end
def s.id
#data[:student][:id]
end
s.data = #data
puts s.id
This code does the equivalent of your own answer, with some improvements. (Only by reading that did I realize what you were trying to accomplish.) To avoid being overly complex, I tried to avoid dynamically generating method names.
#!/usr/bin/env ruby
require 'forwardable'
#data = {
:student => {
:id => '123477',
:first_name => 'Lazlo',
:last_name =>'Fortunatus',
:email=>'Lazlo#fortunatus.org'
},
:contact_info => {
:telephone=>'1 415 222-2222',
:address => '123 Main St',
:city =>'Beverly Hills',
:state=>'California',
:zip_code=>90210,
:social_security_number =>'111-11-1111'
}
}
class ContactInfo
def initialize( data )
#data = data
end
def get_telephone_override
#data[:telephone]
end
def get_city_override
#data[:city]
end
def get_state_override
#data[:state]
end
def get_zip_code_override
#data[:zip_code]
end
def get_social_security_number_override
#data[:social_security_number]
end
end
class Student
extend Forwardable # enables delegation (see ruby-doc.org's standard library)
# delegates multiple methods to #contact_info, so they can be called on Student.
# Remember to have the leading colon.
def_delegators :#contact_info,
:get_telephone_override,
:get_city_override,
:get_state_override,
:get_zip_code_override,
:get_social_security_number_override
def initialize( data )
#data = data[:student]
# this is an example of composing objects to achieve separation of concerns.
# we use delegators so ContactInfo methods are available on the Student instance.
#contact_info = ContactInfo.new(data[:contact_info])
end
def get_id_override
#data[:id]
end
def get_first_name_override
#data[:first_name]
end
def get_last_name_override
#data[:last_name]
end
def get_email_override
#data[:email]
end
end
s = Student.new(#data)
class << s
alias_method :id, :get_id_override
alias_method :first_name, :get_first_name_override
alias_method :last_name, :get_last_name_override
alias_method :email, :get_email_override
alias_method :contact_info, :get_telephone_override
alias_method :city, :get_city_override
alias_method :state, :get_state_override
alias_method :zipcode, :get_zip_code_override
alias_method :ssn, :get_social_security_number_override
end
puts s.id
puts s.first_name
puts s.last_name
puts s.email
puts s.contact_info
puts s.city
puts s.state
puts s.zipcode
puts s.ssn
I think your question would've been clearer if you posted the code as you wanted it to work. I'm going to suggest an edit.
Should you be defining an instance variable (prefixed by "#") outside of a class definition?
Also, you can't define a method with a period in the name