testing polymorphic association in rails - ruby-on-rails

I was writing test for updating images in my rails application. The pictures model in my app is a polymorphic association and it belongs to more than one model. I am testing the create, update and destroy action workflow in the integration tests. I have tested the create action successfully. Its working fine. The problem I am having is with the update action.
The model association with the pictures model is given below.
class Picture < ActiveRecord::Base
belongs_to :pictureable, polymorphic: true
# each user has one picture
has_one :picture, as: :pictureable
#each scoreboard has one picture
has_one :picture, as: :pictureable, dependent: :destroy
The code for the picture_update_test is given below.
def setup
#picture = pictures(:picture_a) #this is the picture associated with pictureable (scoreboard_a)
#scoreboard = scoreboards(:scoreboard_a) #pictureable is scoreboard_a
end
test "successful update where pictureable is scorebaord" do
patch scoreboard_picture_path(#scoreboard, #picture), picture: {picture: "blank-prof.jpg"}
end
The code in the picture.yml file is given below.
picture_a:
picture: "blank-prof.jpg"
pictureable_type: scoreboard
Once I run the tests, I get the following error.
NoMethodError: undefined method `update_attributes' for nil:NilClass
app/controllers/pictures_controller.rb:19:in `update'
The picture controller code is also given below.
def update
#picture = #pictureable.picture
if #picture.update_attributes(picture_params)
redirect_to #pictureable
flash[:success] = "Picture updated successfully"
else
redirect_to #pictureable
flash[:danger] = "An error occured, please try again!"
end
end
I find out if #pictureable is a user or a scoreboard by splitting the url. The code is given below.
def load_pictureable
resource, id = request.path.split('/')[1,2]
#pictureable = resource.singularize.classify.constantize.find(id)
end
I run a before_filter for that method. before_filter :load_pictureable.
In the tests, I don't need to split any url. I am specifically stating the url path, scoreboard_picture_path or 'user_picture_path'. I just have to pass the scoreboard_id and the picture.id. The error states update_attributes for nil class. I am really not sure why I am getting this error. I am not sure if pictures.yml file is correct in terms of the association. As always, any help would be greatly appreciated. Thanks!!

In your yaml you're setting pictureable_type but not picturable_id and so the association is not being built correctly.
You can see the fixtures docs for examples of setting up polymorphic association fixtures, but basically instead of what you're doing you should be able to do something like:
picture_a:
picture: "blank-prof.jpg"
pictureable: scoreboard_a (Scoreboard)
That should then allow #pictureable.picture to return the picture.

Related

How to populate example data to my Rails application in production?

I have a n00b question. I'm using Rails 5, and would like to have example data in the application. When a user creates a new project, the project should already contain sample "tasks" that the user can delete or edit.
I know I can use seeds.rb to create sample data in my development environment. What is the best way to do it in a production environment for new users, and how? Should I use seeds.rb, a migration, or a rake task?
Example controller:
def create
#project = Project.new(project_params)
#project.user = current_user
if #project.save
// add sample content
redirect_to #project
else
render :new
end
end
In the Project model:
belongs_to :user
has_many :tasks, dependent: :destroy
When a new user joins and creates a new project, how do I add sample "tasks" automatically on the new project that the user creates?
UPDATE:
To create a task, I need a description and the current user's id (I'm using Devise, so I can use the current_user helper), for example:
#project.tasks.create!(description: "hello", user_id: current_user.id)
You could build a simple ServiceObject that does the job. It allows you to keep your controller skinny and you can user your current_user Devise helper to keep track of which user is creating the project
if #project.save
SetupBaseProject.new(project).call
redirect_to #project
else
# handle errors
end
In app/services/setup_base_project.rb
class SetupBaseProject
def initialize(project, user)
#project = project
end
def call
# Create example tasks and any additional setup you want to add
#project.tasks.create(description: 'Hello World', user: #project.user)
end
end
There are two possible scenarios considering your question.
The first project created by a user needs to have sample tasks included by default
Whenever a new project is created, sample tasks are created by default. Irrespective of the user is new user/existing user.
For first scenario,
We need to track whether project is created by new user by adding a boolean field to user, for example: new_user which defaults true.
We can use active record callbacks for generating sample tasks after project is created.
For Example,
Project Model :
belongs_to :user
has_many :tasks, dependent: destroy
after_create :generate_tasks
def generate_tasks
if self.user.new_user #This conditional block can be modified as needed
(1..3).each do |n|
self.tasks.create!(description: "Sample task #{n}", user_id: self.user.id)
end
end
end
For the second scenario,
We can use the same projects model file and just remove the conditional statement which will help create sample tasks by after project is created.
If you need any clarification, please comment out.
I've done this quite a few times in the past.
From my experience, you eventually have to give other people the ability to manage those defaults (product owners, marketing, etc)
What I've done in the past is to have a test user with a project that acts as 'the default' project.
Whenever anyone wants to create a new project, you clone it.
I used https://github.com/amoeba-rb/amoeba for that. It offers out of the bow way to override attributes that I'd want to change and can cascade the cloning to any associations you'd want to clone.
Say sample data is on model Detail which was populated with seeds.rb and belongs to 'Project'. You can dup that record and asign it to the new project (not tested):
def create
#project = Project.new(project_params)
#project.user = current_user
#project.details << Detail.find_by_name('sample').dup
if #project.save
redirect_to #company
else
render :new
end
end
Also, consider use a transaction when saving data on more than one model.
Full disclosure, I work in Rails 4...
If it were me, I would use FactoryBot to get the dummy data you want. Factories are great for testing so if you use them for testing, why not borrow them for this? This post shows an example where someone wanted to mock dummy data in console, same ideas could apply for you here.
Once you've got your factories mocked up... maybe for tasks something like:
require 'faker'
FactoryBot.define do
factory :task do
transient do
parent_project { nil }
end
description { Faker::Hacker.say_something_smart }
project_id { parent_project.id }
end
end
Maybe create a method in the project model like:
def create_dummy_data
require 'factory_bot'
require 'faker'
include FactoryBot::Syntax::Methods
# create_list will spit out 3 tasks associated with your project
create_list(:task, 3, parent_project: self)
end
Then in your example: after calling save...
if #project.save
#project.create_dummy_data
redirect_to #company
else
I can't think of a reason you couldn't go this route... noodling around in console I didn't have any problems, but I'd look at this answer as a starting point and not a final solution =P

Devise: Associate record on login

I added a custom devise sessions controller to associate records after someone logged in:
class SessionsController < Devise::SessionsController
before_create :associate_calculation
def associate_calculation
Calculation.find(self.calculation_id).user_id = self.id
Calculation.last.save!
end
end
Here are the request parameters:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"JOQQcCTB9tkVegDgHP/ww8hu5qSzNWlu+4HZZ9AmQGYVO60f3BliwEYT+HKAGPsOOqbipSgj/xSqcDLqueOPZw==", "user"=>{"calculation_id"=>"48759708645478633", "email"=>"jonas#slooob.com", "password"=>"[FILTERED]", "remember_me"=>"0"}, "commit"=>"Signin"}
I also added attr_accessor :calculation_id to the User model.
Still, the corresponding calculation does not get associated to the signed in user.
I also tried to manually attach a calculation to a user:
class SessionsController < Devise::SessionsController
before_create :associate_calculation
def associate_calculation
Calculation.last.user_id = User.first.id
Calculation.last.save!
end
end
That did not work either.
def associate_calculation
c = Calculation.find(self.calculation_id)
c.user_id = self.id #self.id is probably also wrong. maybe current_user?
#Calculation.save! You can not save a class. You need to save an instance
c.save
end
Save the object not the class. You should have had an error so also make sure the method is actually being called by adding a trace to your log file and check the output.
I also suspect that self.id may not be what you are looking for either. Perhaps you should be looking for current_user.id? or whatever the name of the currently logged in user object is at this point. Also if you prefer using the model relationships then maybe something like
c.user = current_user
c.save
The above all assumes that you have the correct associations e.g. user has_many calculations and calculation belongs_to user and that your database reflects this. I say this because you mentioned:
I also added attr_accessor :calculation_id to the User model.
Which is clearly wrong and would never give you the desired results so it looks like you have a little confusion
just adding a method to your model would not have any effect at all. Even if it did work your solution would only ever allow one calculation for a user yet your code implies you would have many calculations.
This leads me on to think that you should not just be finding a calculation but should be creating one?
Did you set your routes properly? For the extended controller to take effect you would need somethign like
class SessionsController < Devise::SessionsController
def destroy
reset_session
super
end
end
# In your routes
devise_for :users do
get "/users/sign_out", :to => "sessions#destroy", :as => "destroy_user_session"
end

Redirect to child record after creating it within a parent parent model

I have three models: Stockholder and Folder; each Stock has_many Stockholders, each Stockholder has_one :folder. The Folder record is created within the Stockholder model like so.
before_create :build_default_folder
private
def build_default_folder
logger.debug "The debug #{self.holder_index}"
build_folder(name: "#{self.holder_index}. #{self.holder_name}", company_id: self.stock.company.id, parent_id: self.stock.company.folders.find_by_name("#{self.stock.security_series} #{self.stock.security_class} Stocks").id)
true
end
Upon submission of the form in which information for stockholders are entered, I would like to redirect to the folder that was just created. And am attempting to do so with the following redirect:
#stockholder=Stockholder.find(params[:id])
if #stockholder.update(stockholder_params)
redirect_to company_browse_path(#stockholder.stock.company, #stockholder.folder.id)
end
This, however, results in the following error:
Undefined method `id' for nil:NilClass
There is no issue, however, if the folder already exists and I'm simply editing the stockholder. This leads me to believe that folder is not created in time for the controller to be able to use it as a parameter in the redirect. How might I work around this?
I don't know exactly what you are trying to do, but it looks like there is some stuff missing. First, you need to associate the Folder with a Stockholder, the next thing is, your Folder needs to be saved. I don't know what build_folder does as I've never seen that before but here is what works for me:
before_create :build_default_folder
private
def build_default_folder
folder = Folder.new(stockholder_id: self.id, name: "a name")
folder.save
end
Figured it out, had to enter the redirect as follows:
redirect_to company_browse_path(#stockholder.stock.company, Folder.find_by(stockholder: #stockholder))

Strong parameters issue with custom controller?

I'm using acts_as_taggable_on plugin in conjunction with my User model.
acts_as_taggable_on :skills
Now, I have a custom controller called SkillsController to add skills via ajax.
class SkillController < ApplicationController
def add
current_user.skill_list.add(params[:skill])
current_user.save # Not saving!
end
end
and in routes.rb
get 'skill/:skill', to: 'skill#add'
I guess it has to do something with Strong Parameters, but I don't know how to solve it as it stands.
The current_user.save isn't working, how to solve it.
P.S current_user.errors shows #message is "too short" as per my validations. But how do I just save the skill_list without having to modify other attributes or running validations on them?
If you want to save current_user without validation check you can do just like as:
current_user.save(:validate => false)
This will work for you :)

Devise/OmniAuth not signing in some users

Our product is a Rails application; authentication is handled with Devise and OmniAuth. ~2000 users total. We've recently had reports of some users not being able to sign in, but we can't figure out why. Not getting any server errors or anything in our production logs to suggest anything is awry.
Let's look at some code…
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
...
def twitter
oauthorize "twitter"
end
private
def oauthorize(provider)
if env['omniauth.auth']
#identity = Identity.from_omniauth(env['omniauth.auth'])
#person = #identity.person
# 1. failing here? Maybe?
if #person
PersonMeta.create_for_person(#person, session[:referrer], session[:landing_page]) if #person.first_visit?
# 2. PersonMetas *aren't* being created.
flash[:notice] = I18n.t("devise.omniauth_callbacks.success", kind: provider)
sign_in_and_redirect(#person, :event => :authentication)
# 3. Definitely failing by here…
else
redirect_to root_url
end
else
redirect_to root_url
end
end
end
class Identity < ActiveRecord::Base
belongs_to :person, counter_cache: true, touch: true
after_create :check_person
def self.from_omniauth(auth)
where(auth.slice("provider", "uid")).first_or_initialize.tap do |identity|
identity.oauth_token = auth['credentials']['token']
identity.oauth_secret = auth['credentials']['secret']
case auth['provider']
when "twitter"
identity.name = auth['info']['name']
identity.nickname = auth['info']['nickname']
identity.bio = auth['info']['description']
identity.avatar_address = auth['info']['image']
else
raise "Provider #{provider} not handled"
end
identity.save
end
end
def check_person
if person_id.nil?
p = create_person(nickname: nickname, name: name, remote_avatar_url: biggest_avatar)
p.identities << self
end
end
def biggest_avatar
avatar_address.gsub('_bigger', '').gsub('_normal', '') if avatar_address
end
end
class PersonMeta < ActiveRecord::Base
attr_accessible :landing_page, :mixpanel_id, :referrer_url, :person_id
belongs_to :person
def self.create_for_person(person, referrer, landing_page)
PersonMeta.create!(referrer_url: referrer, landing_page: landing_page, person_id: person.id)
end
end
So we have that, and we're not getting any errors in production.
Where do we start? Well, let's see if the point of failure is Identity.from_omniauth
This method searches for an existing identity (we've written extra code for more providers, but not implemented client-side yet). If no identity is found it will create one, and then create the associated Person model. If this was the point of failure we'd be able to see some suspiciously empty fields in the production console. But no - the Person & Identity models have all been created with all of the correct fields, and the relevant bits of the app have seen them (e.g. their 'user profile pages' have all been created).
I just added in the if #person to the #oauthorize - we had one 500 where #identity.person was nil, but haven't been able to replicate.
Anyway, the real-world users in question do have complete models with associations intact. Moving down the method we then create a PersonMeta record to record simple stuff like landing page. I'd have done this as an after_create on the Person but I figured it wasn't right to be passing session data to a model.
This isn't being created for our problematic users. At this point, I'm kind of stumped. I'm not sure how the create ! (with bang) got in there, but shouldn't this be throwing an exception if somthing's broken? It isn't.
That is only called if it's a person's first visit anyway - subsequent logins should bypass it. One of the problematic users is a friend so I've been getting him to try out various other things, including signing in again, trying different browsers etc, and it keeps happening
so anyway, after spending 45 minutes writing this post…
One of the users revoked access to the app via Twitter and reauthenticated. Everything works now.
What the hell?
His old identity had his OAuth tokens etc stored properly.
Luckily this is resolved for one user but it's obviously an ongoing problem.
What do we do?
Is it possible that the identity.save line in Identity.from_omniauth is failing silently? If so, your after_create hook won't run, #identity.person will be nil, and you'll just (silently) redirect.
Try identity.save! ?

Resources