Setup fake data just once - ruby-on-rails

I'm working on a rails application with no models. I have a class in lib, FakeMaker, that builds up a bunch of fake entities for display purposed.
I want to test out deletion functionality but the problem is that my fake data set re-initializes every time I hit the controller.
I'd like to run the data test creator only once so that I only have one set of fake data.
I have tried using ||=, before_filter, class methods in FakeMaker, sessions storage but they all seem to have the issue of reinitializing the data set everytime the controller is hit.
Controller code:
class HomeController < ApplicationController
include FakeMaker
before_filter :set_up_fake_data
def index
#workstations = #data[:workstations]
#data_sources = #data[:data_sources]
end
private
def set_fake_data
#data ||= session[:fake_data]
end
def initialize_data
session[:fake_data] = set_up_fake_data
end
end
FakeMaker in lib:
module FakeMaker
include ActionView::Helpers::NumberHelper
SOURCE_CIDNE = "CIDNE"
SOURCE_DCGS = "DCGS"
TYPES_CIDNE = Faker::Lorem.words(num = 10)
TYPES_DCGS = Faker::Lorem.words(num = 4)
def set_up_fake_data
#data ||= { workstations: fake_maker("workstation", 8), data_sources: fake_maker("data_source", 2) }
end
def fake_maker(type, count)
fake = []
case type
when "report"
count.times { fake << fake_report }
when "workstation"
count.times { fake << fake_workstation }
when "data_source"
fake = fake_data_source
end
fake
end
def fake_report
report = { source: [SOURCE_CIDNE, SOURCE_DCGS].sample,
count: number_with_delimiter(Faker::Number.number(5), :delimiter => ',') }
report[:type] = report[:source] == SOURCE_CIDNE ? TYPES_CIDNE.sample : TYPES_DCGS.sample.capitalize
report
end
def fake_workstation
{ name: Faker::Lorem.word,
downloaded: number_with_delimiter(Faker::Number.number(3), :delimiter => ','),
available: number_with_delimiter(Faker::Number.number(5), :delimiter => ','),
last_connect: fake_time,
queueJSON: fake_queueJSON,
historyJSON: fake_historyJSON }
end
def fake_data_source
data_sources = []
["CIDNE", "DCGS"].each do |source|
data_sources << { type: source,
name: Faker::Internet.url,
status: ["UP", "DOWN"].sample }
end
data_sources
end
def fake_historyJSON
history = []
12.times { history << fake_history }
history
end
def fake_queueJSON
queue = []
35.times { queue << fake_queue }
queue
end
def fake_history
{ sentTimestamp: fake_time,
reportID: Faker::Number.number(5)}
end
def fake_queue
{ priority: Faker::Number.number(3),
queuedTimestamp: fake_time,
reportID: Faker::Number.number(5)}
end
def fake_time
Random.rand(10.weeks).ago.strftime("%Y-%m-%d %H:%M:%S")
end
end

Related

Rails 5 manage result from monads

I've got Rails 5 app with dry-monads on board. Monads are used to create the Appointment object inside create action in AppointmentsController. They return Success or Failure in the last step with below structure:
# services/appointments/create.rb
(...)
def call
Success(appointment_params: appointment_params)
(...)
.bind(method(:save_appointment))
end
private
def save_appointment(appointment)
if appointment.save
Success(appointment)
else
Failure(failure_appointments: appointment, appointments_errors: appointment.errors.full_messages)
end
end
After each action (success or failure) I want to send an email and display the corresponding json in AppointmentsController:
class Api::AppointmentsController < ApplicationController
def create
succeeded_appointments = []
failure_appointments = []
appointments_errors = []
batch_create_appointments_params[:_json].each do |appointment_params|
appointment = ::Appointments::Create.new(appointment_params).call
if appointment.success?
succeeded_appointments << appointment.value!
else
failure_appointments << appointment.failure[:failure_appointments] &&
appointments_errors << appointment.failure[:appointments_errors]
end
end
if failure_appointments.any?
AppointmentMailer.failed_mail(email, failure_appointments.size, appointments_errors).deliver_now
render json: {
error: appointments_errors.join(', '),
}, status: :bad_request
elsif succeeded_appointments.any?
AppointmentMailer.success_mail(email, succeeded_appointments.size).deliver_now
render json: {
success: succeeded_appointments.map do |appointment|
appointment.as_json(include: %i[car customer work_orders])
end,
}
end
end
I wonder if there is a better, faster way to record these errors than declaring 3 different empty arrays (succeeded_appointments, failure_appointments, appointments_errors) like at the beginning of create action? so far the create action looks heavy.
Create a separate service object for bulk creation:
# services/appointments/bulk_create.rb
class Appointments::BulkCreate
def initialize(bulk_params)
#bulk_params = bulk_params
end
def call
if failed_results.any?
AppointmentMailer.failed_mail(email, failed_results_errors.size, failed_results_errors).deliver_now
Failure(failed_results_errors.join(', '))
else
AppointmentMailer.success_mail(email, success_appointments.size).deliver_now
Success(success_appointments)
end
end
private
attr_reader :bulk_params
def failed_results
results.select(&:failure?)
end
def success_results
results.select(&:success?)
end
def success_appointments
#success_appointments ||= success_results.map do |appointment|
appointment.as_json(include: %i[car customer work_orders])
end
end
def failed_results_errors
#failed_results_errors ||= failed_results.map do |failed_result|
failed_result.failure[:appointments_errors]
end
end
def results
#results ||= bulk_params.map do |appointment_params|
::Appointments::Create.new(appointment_params).call
end
end
end
Then your controller will look like this:
class Api::AppointmentsController < ApplicationController
def create
result = ::Appointments::BulkCreate.new(batch_create_appointments_params[:_json]).call
if result.success?
render json: { success: result.value! }, status: :ok
else
render json: { error: result.failure }, status: :bad_request
end
end
end

undefined method `map' api request [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 4 years ago.
Improve this question
I followed tutorial how to integrate 3rd party api with a ruby on rails but I get an error
undefined method `map' for
{"number"=>12} permitted: false>:ActionController::Parameters
which points to request.rb
query_string = query.map{|k,v| "#{k}=#{v}"}.join("&")
Full code
recipes_controller.rb
class RecipesController < ApplicationController
def index
#tag = query.fetch(:tags, 'all')
#refresh_params = refresh_params
#recipes, #errors = Spoonacular::Recipe.random(query, clear_cache)
end
def show
#recipe = Spoonacular::Recipe.find(params[:id])
end
private
def query
params.permit(:query).fetch(:query, {})
end
def clear_cache
params[:clear_cache].present?
end
def refresh_params
refresh = { clear_cache: true }
refresh.merge!({ query: query }) if query.present?
refresh
end
end
app/services/spoonacular/recipes.rb
module Spoonacular
class Recipe < Base
attr_accessor :aggregate_likes,
:dairy_free,
:gluten_free,
:id,
:image,
:ingredients,
:instructions,
:ready_in_minutes,
:title,
:vegan,
:vegetarian
MAX_LIMIT = 12
CACHE_DEFAULTS = { expires_in: 7.days, force: false }
def self.random(query = {}, clear_cache)
cache = CACHE_DEFAULTS.merge({ force: clear_cache })
response = Spoonacular::Request.where('recipes/random', cache, query.merge({ number: MAX_LIMIT }))
recipes = response.fetch('recipes', []).map { |recipe| Recipe.new(recipe) }
[ recipes, response[:errors] ]
end
def self.find(id)
response = Spoonacular::Request.get("recipes/#{id}/information", CACHE_DEFAULTS)
Recipe.new(response)
end
def initialize(args = {})
super(args)
self.ingredients = parse_ingredients(args)
self.instructions = parse_instructions(args)
end
def parse_ingredients(args = {})
args.fetch("extendedIngredients", []).map { |ingredient| Ingredient.new(ingredient) }
end
def parse_instructions(args = {})
instructions = args.fetch("analyzedInstructions", [])
if instructions.present?
steps = instructions.first.fetch("steps", [])
steps.map { |instruction| Instruction.new(instruction) }
else
[]
end
end
end
end
app/services/spoonacular/base.rb
module Spoonacular
class Base
attr_accessor :errors
def initialize(args = {})
args.each do |name, value|
attr_name = name.to_s.underscore
send("#{attr_name}=", value) if respond_to?("#{attr_name}=")
end
end
end
end
app/services/spoonacular/request.rb
module Spoonacular
class Request
class << self
def where(resource_path, cache, query = {}, options = {})
response, status = get_json(resource_path, cache, query)
status == 200 ? response : errors(response)
end
def get(id, cache)
response, status = get_json(id, cache)
status == 200 ? response : errors(response)
end
def errors(response)
error = { errors: { status: response["status"], message: response["message"] } }
response.merge(error)
end
def get_json(root_path, cache, query = {})
query_string = query.map{|k,v| "#{k}=#{v}"}.join("&")
path = query.empty?? root_path : "#{root_path}?#{query_string}"
response = Rails.cache.fetch(path, expires_in: cache[:expires_in], force: cache[:force]) do
api.get(path)
end
[JSON.parse(response.body), response.status]
end
def api
Connection.api
end
end
end
end
app/services/spoonacular/connection.rb
require 'faraday'
require 'json'
module Spoonacular
class Connection
BASE = 'https://spoonacular-recipe-food-nutrition-v1.p.mashape.com'
def self.api
Faraday.new(url: BASE) do |faraday|
faraday.response :logger
faraday.adapter Faraday.default_adapter
faraday.headers['Content-Type'] = 'application/json'
faraday.headers['X-Mashape-Key'] ='key'
end
end
end
end
Thank you for any help.
You have 2 separate errors here.
uninitialized constant Spoonacular::Recipe::Request
This one you can fix by explicitly setting top-level scope for Request class:
::Request.where(...)
It applies if you keep Request file in app/spoonacular/request.rb. But I suggest to move it to app/services/spoonacular/ where all your other spoonacular related classes are. So in this case you need to encircle class Request in module Spoonacular. After that you can call it like that:
Spoonacular::Request.where(...)
Same goes for class Connection.
SO answer about scope resolution operator
undefined method `map' for {"number"=>12} permitted:
false>:ActionController::Parameters
This one comes from private query method in recipes_controller.rb. params is ActionController::Parameters object and in order to retrieve values from it you need to permit them first:
def query
params.permit(:query).to_h
end
Now it should return Hash object.
Here is detailed answer on SO about that
RubyOnRails Guide about strong params

How to refactor so many conditionals statements?

I've a class to compare the imported data with the database values. I ended up with so much conditions that rubocop was shouting loud for it. So I broke the method into smaller methods but there are still conditionals in those methods. Here is the code:
Before
class Utility
attr_reader :im_data, :db_data
def initialize(im_data, db_data)
#im_data = im_data
#db_data = db_data
#to_update = []
#to_delete = []
end
def compare_values
if !im_data[:name].present?
#to_delete << im_data[:name]
elsif im_data[:name].present?
if im_data[:lookup].present? && (im_data[:lookup] != db_data.full_name)
#to_update << { id: im_data[:l_v_id], full_name: im_data[:lookup] }
elsif !im_data[:lookup].present? && (im_data[:name] != db_data.full_name)
#to_update << { id: im_data[:l_v_id], full_name: im_data[:name] }
end
end
end
end
After
def compare_values(im_data, db_data)
deselection(im_data)
re_apply(im_data, db_data)
end
def presence?(value)
value.present?
end
def deselection(im_data)
#to_delete << im_data[:l_v_id] unless presence?(im_data[:name])
end
def re_apply(im_data, db_data)
fv_present = presence?(im_data[:name])
compare_lookup(im_data, db_data.full_name) if fv_present
compare_name(im_data, db_data.full_name) if fv_present
end
def compare_lookup(im_data, l_value)
#to_update << { id: im_data[:l_v_id], full_name: im_data[:lookup] } if presence?(im_data[:lookup]) && (im_data[:lookup] != l_value)
end
def compare_name(im_data, full_name)
#to_update << { id: im_data[:l_v_id], full_name: im_data[:name] } if !presence?(im_data[:lookup]) && (im_data[:name] != full_name)
end
I tried to follow this blog but no luck with it. I still feel there is a much much better way to refactor this code.
I personally find "after" much harder to follow. How about the following?
def compare_values
if #im_data[:name].present?
to_upd(#im_data[:lookup].present? ? :lookup : :name)
else
#to_delete << #im_data[:name]
end
end
def to_upd(key)
#to_update << { id: #im_data[:l_v_id], full_name: #im_data[key] } unless
#im_data[key] == #db_data.full_name
end

How can I test these RSS parsing service objects?

I have some service objects that use Nokogiri to make AR instances. I created a rake task so that I can update the instances with a cron job. What I want to test is if it's adding items that weren't there before, ie:
Create an Importer with a url of spec/fixtures/feed.xml, feed.xml having 10 items.
Expect Show.count == 1 and Episode.count == 10
Edit spec/fixtures/feed.xml to have 11 items
Invoke rake task
Expect Show.count == 1 and Episode.count == 11
How could I test this in RSpec, or modify my code to be more testable?
# models/importer.rb
class Importer < ActiveRecord::Base
after_create :parse_importer
validates :title, presence: true
validates :url, presence: true
validates :feed_format, presence: true
private
def parse_importer
Parser.new(self)
end
end
# models/show.rb
class Show < ActiveRecord::Base
validates :title, presence: true
validates :title, uniqueness: true
has_many :episodes
attr_accessor :entries
end
# models/episode.rb
class Episode < ActiveRecord::Base
validates :title, presence: true
validates :title, uniqueness: true
belongs_to :show
end
#lib/tasks/admin.rake
namespace :admin do
desc "Checks all Importer URLs for new items."
task refresh: :environment do
#importers = Importer.all
#importers.each do |importer|
Parser.new(importer)
end
end
end
# services/parser.rb
class Parser
def initialize(importer)
feed = Feed.new(importer)
show = Show.where(rss_link: importer.url).first
if show # add new episodes
new_episodes = Itunes::Channel.refresh(feed.origin)
new_episodes.each do |new_episode|
show.episodes.create feed.episode(new_episode)
end
else # create a show and its episodes
new_show = Show.new(feed.show) if (feed && feed.show)
if (new_show.save && new_show.entries.any?)
new_show.entries.each do |entry|
new_show.episodes.create feed.episode(entry)
end
end
end
end
end
# services/feed.rb
class Feed
require "nokogiri"
require "open-uri"
require "formats/itunes"
attr_reader :params, :origin, :show, :episode
def initialize(params)
#params = params
end
def origin
#origin = Nokogiri::XML(open(params[:url]))
end
def format
#format = params[:feed_format]
end
def show
case format
when "iTunes"
Itunes::Channel.fresh(origin)
end
end
def episode(entry)
#entry = entry
case format
when "iTunes"
Itunes::Item.fresh(#entry)
end
end
end
# services/formats/itunes.rb
class Itunes
class Channel
def initialize(origin)
#origin = origin
end
def title
#origin.xpath("//channel/title").text
end
def description
#origin.xpath("//channel/description").text
end
def summary
#origin.xpath("//channel/*[name()='itunes:summary']").text
end
def subtitle
#origin.xpath("//channel/*[name()='itunes:subtitle']/text()").text
end
def rss_link
#origin.xpath("//channel/*[name()='atom:link']/#href").text
end
def main_link
#origin.xpath("//channel/link/text()").text
end
def docs_link
#origin.xpath("//channel/docs/text()").text
end
def release
#origin.xpath("//channel/pubDate/text()").text
end
def image
#origin.xpath("//channel/image/url/text()").text
end
def language
#origin.xpath("//channel/language/text()").text
end
def keywords
keywords_array(#origin)
end
def categories
category_array(#origin)
end
def explicit
explicit_check(#origin)
end
def entries
entry_array(#origin)
end
def self.fresh(origin)
#show = Itunes::Channel.new origin
return {
description: #show.description,
release: #show.release,
explicit: #show.explicit,
language: #show.language,
title: #show.title,
summary: #show.summary,
subtitle: #show.subtitle,
image: #show.image,
rss_link: #show.rss_link,
main_link: #show.main_link,
docs_link: #show.docs_link,
categories: #show.categories,
keywords: #show.keywords,
entries: #show.entries
}
end
def self.refresh(origin)
#show = Itunes::Channel.new origin
return #show.entries
end
private
def category_array(channel)
arr = []
channel.xpath("//channel/*[name()='itunes:category']/#text").each do |category|
arr.push(category.to_s)
end
return arr
end
def explicit_check(channel)
string = channel.xpath("//channel/*[name()='itunes:explicit']").text
if string === "yes" || string === "Yes"
true
else
false
end
end
def keywords_array(channel)
keywords = channel.xpath("//channel/*[name()='itunes:keywords']/text()").text
arr = keywords.split(",")
return arr
end
def entry_array(channel)
arr = []
channel.xpath("//item").each do |item|
arr.push(item)
end
return arr
end
end
class Item
def initialize(origin)
#origin = origin
end
def description
#origin.xpath("*[name()='itunes:subtitle']").text
end
def release
#origin.xpath("pubDate").text
end
def image
#origin.xpath("*[name()='itunes:image']/#href").text
end
def explicit
explicit_check(#origin)
end
def duration
#origin.xpath("*[name()='itunes:duration']").text
end
def title
#origin.xpath("title").text
end
def enclosure_url
#origin.xpath("enclosure/#url").text
end
def enclosure_length
#origin.xpath("enclosure/#length").text
end
def enclosure_type
#origin.xpath("enclosure/#type").text
end
def keywords
keywords_array(#origin.xpath("*[name()='itunes:keywords']").text)
end
def self.fresh(entry)
#episode = Itunes::Item.new entry
return {
description: #episode.description,
release: #episode.release,
image: #episode.image,
explicit: #episode.explicit,
duration: #episode.duration,
title: #episode.title,
enclosure_url: #episode.enclosure_url,
enclosure_length: #episode.enclosure_length,
enclosure_type: #episode.enclosure_type,
keywords: #episode.keywords
}
end
private
def explicit_check(item)
string = item.xpath("*[name()='itunes:explicit']").text
if string === "yes" || string === "Yes"
true
else
false
end
end
def keywords_array(item)
keywords = item.split(",")
return keywords
end
end
end
Before anything else, good for you for using service objects! I've been using this approach a great deal and find POROs preferable to fat models in many situations.
It appears the behavior you're interested in testing is contained in Parser.initialize.
First, I'd create a class method for Parser called parse. IMO, Parser.parse(importer) is clearer about what Parser is doing than is Parser.new(importer). So, it might look like:
#services/parser.rb
class Parser
class << self
def parse(importer)
#importer = importer
#feed = Feed.new(importer)
if #show = Show.where(rss_link: importer.url).first
create_new_episodes Itunes::Channel.refresh(#feed.origin)
else
create_show_and_episodes
end
end # parse
end
end
Then add the create_new_episodes and create_show_and_episodes class methods.
#services/parser.rb
class Parser
class << self
def parse(importer)
#importer = importer
#feed = Feed.new(importer)
if #show = Show.where(rss_link: #importer.url).first
create_new_episodes Itunes::Channel.refresh(#feed.origin)
else
create_show_and_episodes
end
end # parse
def create_new_episodes(new_episodes)
new_episodes.each do |new_episode|
#show.episodes.create #feed.episode(new_episode)
end
end # create_new_episodes
def create_show_and_episodes
new_show = Show.new(#feed.show) if (#feed && #feed.show)
if (new_show.save && new_show.entries.any?)
new_show.entries.each do |entry|
new_show.episodes.create #feed.episode(entry)
end
end
end # create_show_and_episodes
end
end
Now you have a Parser.create_new_episodes method that you can test independently. So, your test might look something like:
require 'rspec_helper'
describe Parser do
describe '.create_new_episodes' do
context 'when an initial parse has been completed' do
before(:each) do
first_file = Nokogiri::XML(open('spec/fixtures/feed_1.xml'))
#second_file = Nokogiri::XML(open('spec/fixtures/feed_2.xml'))
Parser.create_show_and_episodes first_file
end
it 'changes Episodes.count by 1' do
expect{Parser.create_new_episodes(#second_file)}.to change{Episodes.count}.by(1)
end
it 'changes Show.count by 0' do
expect{Parser.create_new_episodes(#second_file)}.to change{Show.count}.by(0)
end
end
end
end
Naturally, you'll need feed_1.xml and feed_2.xml in the spec\fixtures directory.
Apologies for any typos. And, I didn't run the code. So, might be buggy. Hope it helps.

Manually assigning parent ID with has_many/belongs_to association in custom class

I'm using a custom class to make AR instances from Feedjirra. I can't get the children instances to relate to their parent objects.
Show has_many :episodes -
Episode belongs_to :show -
show_id is always nil.
RSpec logs #show.id and #episode.show_id as equal to one another. However when I run episode = Episode.first after running an import in development, the episode has its show_id set to nil.
#show = Show.new
#show.name = #feed.title
#show.description = #feed.description
...
if #show.save
puts "#show.id: #{#show.id}"
end
#episodes = []
#feed.entries.each do |item|
#episodes.push(item)
end
#episodes.each do |item|
#episode = #show.episodes.new
#episode.name = item.title
#episode.description = item.summary
...
if #episode.save
puts "#episode.show_id: #{#episode.show_id}"
end
end
I tried using #episode = #show.episodes.create, as well as #episode = Episode.new with #episode.show_id = #show.id. They all log matching IDs but show_id is still nil on the instances. Every other column is filled in correctly.
I thought the issue may have had to do with using add_foreign_key:
class AddShowToEpisodes < ActiveRecord::Migration
def change
add_reference :episodes, :show, index: true
add_foreign_key :episodes, :shows, column: :show_id
end
end
So I removed that and used the standard foreign_key: true but it had no effect.
class RemoveShowFromEpisodes < ActiveRecord::Migration
def change
remove_column :episodes, :show_id
end
end
class AddShowBackToEpisodes < ActiveRecord::Migration
def change
add_reference :episodes, :show, index: true, foreign_key: true
end
end
Here's the full code in case it helps.
importers_controller.rb:
class Admin::ImportersController < Admin::ApplicationController
before_action :set_importer, only: [:show, :edit, :update, :destroy]
def index
#importers = policy_scope(Importer)
end
def show
end
def new
#importer = Importer.new
authorize #importer
end
def create
#importer = Importer.new(importer_params)
authorize #importer
if #importer.save
require "subscription_importer"
SubscriptionImporter.new(#importer)
flash[:notice] = "Importer added."
redirect_to admin_importers_path
else
flash[:error] = "Importer not added."
render "new"
end
end
def edit
end
def update
end
def destroy
end
private
def set_importer
#importer = Importer.find(params[:id])
authorize #importer
end
def importer_params
params.require(:importer).permit(:name, :url, :source)
end
end
subscription_importer.rb:
class SubscriptionImporter
def initialize(importer)
#importer = importer
#feed = Feedjira::Feed.fetch_and_parse #importer.url
if #importer.source === "iTunes"
itunes_parser(#importer)
end
end
def itunes_parser(importer)
#importer = importer
# Parser
#feed = Feedjira::Feed.fetch_and_parse #importer.url
# Show
#show = Show.new
#show.name = #feed.title
#show.description = #feed.description
#show.logo = #feed.itunes_image
#show.explicit = explicit_check(#feed.itunes_explicit)
#show.genre = #feed.itunes_categories
#show.tags = #feed.itunes_keywords
#show.url = #feed.url
#show.language = #feed.language
if #show.save
puts "Show import succeeded"
puts "#show.id: #{#show.id}"
else
puts "Show import failed"
end
# Episodes
#episodes = []
#feed.entries.each do |item|
#episodes.push(item)
end
#episodes.each do |item|
#episode = #show.episodes.new
#episode.name = item.title
#episode.description = item.summary
#episode.release = item.published
#episode.image = item.itunes_image
#episode.explicit = explicit_check(item.itunes_explicit)
#episode.tags = item.itunes_keywords
#episode.url = item.enclosure_url
#episode.duration = item.itunes_duration
if #episode.save
puts "Episode import succeeded"
puts "#episode.show_id: #{#episode.show_id}"
else
puts "Episode import failed"
end
end
end
def explicit_check(string)
if string == "yes" || "Yes"
true
else
false
end
end
end
create_importer_spec.rb:
require "rails_helper"
RSpec.feature "Admins can create importers" do
let(:user) { FactoryGirl.create(:user, :admin) }
context "admins" do
before do
login_as(user)
visit "/"
click_link "Admin"
click_link "Importers"
click_link "New Importer"
end
scenario "with valid credentials" do
fill_in "Name", with: "The Stack Exchange Podcast"
fill_in "Url", with: "https://blog.stackoverflow.com/feed/podcast/" # Needs stubbing
select "iTunes", from: "Source"
click_button "Create Importer"
expect(page).to have_content "Importer added"
expect(page).to have_content "The Stack Exchange Podcast"
end
scenario "with invalid credentials" do
fill_in "Name", with: ""
fill_in "Url", with: ""
click_button "Create Importer"
expect(page).to have_content "Importer not added"
end
end
end
I think the episodes functionality in your SubscriptionImporter class is causing the problem...
#episodes = []
#feed.entries.each do |item|
#episodes.push(item) #-> each "#episodes" is a FeedJirra object
end
#episodes.each do |episode|
#-> you're now creating an episode in the same call as show, which will either mean that show is not persisted or perhaps some other error
end
I would personally limit the SubscriptionImporter functionality to only return data. You should be parsing that data through the respective models:
#app/controllers/admin/importers_controller.rb
class Admin::ImportersController < Admin::ApplicationController
def create
#import = Importer.new import_params
if #import.save
#import.parse_show if #import.itunes?
end
end
private
def import_params
params.require(:importer).permit(:name, :url, :source)
end
end
#app/models/importer.rb
class Importer < ActiveRecord::Base
def feed
return false unless itunes?
origin = Feedjirra::Feed.fetch_and_parse(self.url)
return {
name: origin.title,
description: origin.description,
logo: origin.itunes_image,
explicit: explicit_check(origin.itunes_explicit),
genre: origin.itunes_categories,
tags: origin.itunes_keywords,
url: origin.url,
language: origin.language,
entries: origin.entries
}
end
def parse_show
Show.create(feed)
end
def itunes?
self.source == "iTunes" #-> true/false
end
private
def explicit_check
string == "yes" || "Yes" #-> true/false
end
end
#app/models/show.rb
class Show < ActiveRecord::Base
has_many :episodes
attr_accessor :entries
after_create :create_episodes #-> might not persist entries
def create_episodes
if self.entries.any?
self.entries.each do |item|
self.episodes.create({
name: item.title
description: item.summary,
release: item.published,
image: item.itunes_image,
explicit: explicit_check?(item.itunes_explicit),
tags: item.itunes_keywords,
url: item.enclosure_url,
duration: item.itunes_duration
})
end
end
end
private
def explicit_check?
string == "yes" || "Yes"
end
end
The above will allow you to create an #importer, pull the feed from it, and populate Show & Episode models with the returned data.
Whilst this should resolve your issue, you need to consider OOP -- making each element an object.
Update
If you wanted to objectify this even more, there is a simple pattern to adopt:
Importer is all you need to save -- everything else should happen around this
Show + Episode could be the same class / table for all I know
With this in mind, you could do the following:
#app/controllers/admin/importers_controller.rb
class Admin::ImportersController < Admin::ApplicationController
def create
#import = Importer.new import_params
#import.save
end
private
def import_params
params.require(:importer).permit(:name, :url, :source)
end
end
#app/services/feed.rb
class Feed
attr_reader :params, :show, :episode, :origin
def initialize(params)
#params = params
end
def origin
#origin = Feedjirra::Feed.fetch_and_parse params[:source]
end
def show
#show = ShowHelper.new #origin
end
def episodes
#show.episodes
end
end
#app/services/show_helper.rb
class ShowHelper
attr_reader :origin
def initialize(origin)
#origin = origin
end
def name
#origin.title
end
def description
#origin.summary || #origin.description
end
def logo
#origin.itunes_image
end
def explicit
%r{^yes$} =~ #origin.itunes_explicit
end
def genre
#origin.itunes_categories
end
def tags
#origin.itunes_keywords
end
def url
#origin.url
end
def language
#origin.language
end
def episodes
#origin.entries
end
end
#app/models/importer.rb
class Importer < ActiveRecord::Base
after_create :parse_show, if: "itunes?"
validates :source, :url, :name, presence: true
def itunes?
source == "iTunes"
end
def feed
#feed = Feed.new(self)
end
private
def parse_show
#show = Show.new(feed.show) if feed && feed.show
if #show.save && #show.entries.any?
#show.entries.each do |entry|
#show.episodes.create ShowHelper.new(entry)
end
end
end
end

Resources