Why is my Rails Minitest Polymorphic Failing? - ruby-on-rails

I have these models:
class User < ActiveRecord::Base
has_one :setting, as: :resource, dependent: :destroy
has_many :services
end
class Setting < ActiveRecord::Base
belongs_to :resource, polymorphic: true
end
class Service < ActiveRecord::Base
belongs_to :user
end
and I have this code snippet from a controller to access them, which works when manually testing (this is a before_action method):
before_action :setup
def get_vars
#setting = Setting.find_by_booking_url #url
#user = #setting.resource
#services = #user.services
end
def setup
#url = params[:url]
#setting = Setting.find_by_url #url
#user = #setting.resource
end
but when using automated testing with these fixtures:
# fixtures/users.yml
james:
email: 'james#example.com'
first_name: 'James'
last_name: 'Soften'
encrypted_password: <%= default_password_digest %>
# fixtures/settings.yml
scott_settings:
resource: james (User)
time_zone: 'Pacific Time (US & Canada)'
url: 'urlsegment'
and this controller test code:
setup do
#user = users(:james)
end
def test_booking_page_success
get :new, url: #user.setting.url
assert_response :success
end
it fails with error:
undefined method `services` for nil:NilClass
which indicates that I cannot access "resource" to get the User from the Setting object.
I checked the variables using byebug (in the middle of running tests) and the Setting object (#user.setting) has a resource_id and resource_name, so I was wondering why I cannot access the user from the Setting object using "resource" attribute when running the tests.
Any ideas?
Thanks!
EDIT: added more controller code
EDIT: added URL field in settings fixture

Try the following before test_booking_page_success
def test_url
assert_not_empty #user.setting.url
end
You are probably passing an empty 'url' parameter... if the fixtures shown are complete and you are not generating url from something else.
Upd The only scenario I can imagine: is that booking_url in scott_settings is set to something other than 'urlsegment' in your fixtures. So it returns other settings with nil resource...

Related

Shrine - Download Endpoint with Account-based authentication

I'm using Shrine with Rails, with the download_endpoint plugin. I have this working with route-based authentication via Devise, but I'd like to go one step further and add account-based authentication so that users can only access their own images.
Rails.application.routes.draw do
get "/img/*rest", to: "download#confirm_response"
end
This uses the URL /img/xxx – but I'm trying to use something like /img/account_id/xxx, so this is my updated routes.rb:
Rails.application.routes.draw do
get "/img/:account_id/*rest", to: "download#confirm_response"
end
Here is my updated Controller, I'm not sure exactly what to amend in my set_rack_response method in order to correctly pull in my account_id. I have the rack_response plugin loaded, is it possible to set this up?
class DownloadController < ApplicationController
def confirm_response
#account = Account.find(params[:account_id])
set_rack_response #account.to_rack_response
end
private
def set_rack_response((status, headers, body))
self.status = status
self.headers.merge!(headers)
self.response_body = body
end
end
#Janko EDIT:
I've updated my routes and controller, I have 2 models that have image files (photos and logos) that I would love to use download_endpoint for both.
I feel like I'm missing where to add :account_id to the download_url itself.
Would love for the URL to be /img/:account_id/xxx but what do you think would work best given this setup?
# Account.rb
class Account < ApplicationRecord
has_many :photos
has_many :logos
end
# Photo.rb
class Photo < ApplicationRecord
belongs_to :account
include PhotoUploader::Attachment(:photo_file)
end
# Logo.rb
class Logo < ApplicationRecord
belongs_to :account
include LogoUploader::Attachment(:logo_file)
end
# Updated download_controller.rb
class DownloadController < ApplicationController
def confirm_response
#account = Account.find(params[:account_id])
#photo = #account.photos.find(params[:photo_id])
set_rack_response #photo.to_rack_response
end
private
def set_rack_response((status, headers, body))
self.status = status
self.headers.merge!(headers)
self.response_body = body
end
end
# Updated config/routes.rb
Rails.application.routes.draw do
get "/img/:account_id/:photo_id/*rest", to: "download#confirm_response"
end
# Updated config/initializers/shrine.rb
Shrine.plugin :download_endpoint, prefix: "img", host: [:cloudfront]
Shrine.plugin :rack_response
# Updated views/photos/show.html.erb
<%= image_tag(#photo.photo_file.download_url) %>

Routing Error undefined local variable or method `applicationController' for main:Object

I'm following Agile Web Development with Rails 5.1 book. And I'm trying to upload a picture file. The example in the book doesn't go into the routes.rb in this section, and I'm clearly not getting this part correct, as when loading the browser I get this error:
Routing Error - undefined local variable or method 'applicationController' for main:Object
routes.rb
Rails.application.routes.draw do
resources :orders
resources :line_items
resources :carts
resources :pictures
root 'store#index', as: 'store_index'
resources :products do
get :who_bought, on: :member
end
get 'upload/new', to: 'upload#get'
end
I have been trying other variations, but always end up with no route matches error:
get 'upload/new', to: 'upload#new' or get 'upload/get', to: 'upload#get'
upload_controller.rb
class UploadController < applicationController
def get //the book uses method 'get', would normally expect 'new'
#picture = Picture.new
end
def picture
#picture = Picture.find(params[:id])
send_data(#picture.data, filename: #picture.name, type: #picture.content_type, disposition: "inline")
end
def save
#picture = Picture.new(picture_params)
if #picture.save
redirect_to(action: 'show', id: #picture.id)
else
render(action: :get)
end
end
def show
#picture = Picture.find(params[:id])
end
private
def picture_params
params.require(:picture).permit(:comment, :upload_picture)
end
end
model/picture.rb
Don't expect the model to be causing an issue with the routes, but adding it for completeness.
class Picture < ActiveRecord::Base
validates_format_of :content_type, with: /|Aimage/, message: "must be a picture"
def uploaded_picture=(picture_field)
self.name = base_part_of(picture_field.original_filename)
self.content_type = picture_field.content_type.chomp
self.data = picture_field.read
end
def base_part_of(file_name)
File.basement(file_name).gsub(/[^|w._-]/, '')
end
end
The issue here is the naming convention that is going wrong. The controller that you are inheriting from is ApplicationController and not applicationController According to the naming convention of rails controller's name is always CamelCase. For more naming convention have a look at this link
Here you need to replace class UploadController < applicationController to class UploadController < ApplicationController
Your writing is incorrect applicationController change to like below
class UploadController < ApplicationController
application to Application
That's it, hope it helps

Rails accepts_nested_attributes_for always creates the nested models, but does not update them

Given the following:
class WebsitesController < ApplicationController
# POST /websites/save
# POST /websites/save.json
def save
Website.exists?(name: params[:website][:name]) ? update : create
end
# POST /websites
# POST /websites.json
def create
#server = Server.find_or_create_by_name(params[:server_id])
#website = #server.websites.new(params[:website])
#etc... #website.save
end
# PUT /websites/1
# PUT /websites/1.json
def update
#website = Website.find_by_name(params[:website][:name])
#etc... #website.update_attributes
end
end
The client does not have any IDs of these models
The request that gets sent only has the names, but not the ids.
And the following models
class Website < ActiveRecord::Base
serialize :website_errors
attr_accessible :plugins_attributes
has_many :plugins
accepts_nested_attributes_for :plugins
end
class Plugin < ActiveRecord::Base
belongs_to :website
end
When I make a POST request to /websites/save.json, the Website gets updated correctly if it exists, but the Plugins that belong to it always get recreated causing duplicate content in the Database. Why does this happen? I redirect to the update action which calls update_attributes so how can it be that it does not update it? I take it that it's because no ID is given with the request.
Can I make the Controller listen to plugin_name instead of plugin_id?
Modify your controller to have this:
def update
#website = Website.find_by_name(params[:website][:name])
if #website.update(params)
redirect_to website_path(#website)
else
render :edit
end
end
Also, if you're using strong_parameters, you'll need this at the bottom of your controller:
params.require(:website).
permit(
:name,
...,
plugins_attributes: [
:name,
...,
]
)
end

Rails: undefined method `capitalize' for nil:NilClass

I'm having a store controller like this :
class StoreController < ApplicationController
helper_method :product_type
def store
cat=params[:cat]
subcat=params[:subcat]
if cat.empty?
#store=SubCategory.find_by_name(params[:subcat]).products.where(product_type: params[:type]).paginate(:page => params[:page], :per_page => 12)
#subcat=params[:subcat]
#cat=""
else
#store=Category.find_by_name(params[:cat]).products.where(product_type: params[:type]).paginate(:page => params[:page], :per_page => 12)
#cat=params[:cat]
#subcat=""
end
product_type
end
def show
#show=Product.where(product_type: params[:type]).paginate(:page => params[:page], :per_page => 12)
product_type
end
def product
#product=Product.find(params[:product_id])
product_type
end
def product_type
#type=params[:type]
end
end
In any case, I'm calling the show action first. So, the #type variable gets it's value there itself. So that the #type variable be available in all, methods I have called the product_type method in all other methods.
I'm calling all these variables form a partial dropdown which is a dropdown list and common to the whole site,like a header, like this:-
<%=link_to "T-shirt", store_path(cat:"",subcat:"tshirts",type: #type) %>
The problem is, I'm able to navigate through this dropdown links in all the methods of StoreController except the product method.Meaning, when I call the links in dropdown list via the store,show pages it works fine but it throws the following error when called via product page.
undefined method `capitalize' for nil:NilClass
The #type contains a string which I'm capitalizing. Where am I doing wrong?
My store.html.erb has this line
<h1><%= #type.capitalize+"'s " + #subcat.capitalize + #cat.capitalize %></h1>
The error is telling you that you are calling capitalize method on nil.
I see 2 potentials issues here:
Using find_by_name
What if params[:subcat] is nil or params[:cat] is nil
Using find_by_name will not raise an exception if no record is found, nil will be returned.
Your conditions checking if params[:cat] or params[:subcat] are defined are not right. nil value can occur if some params are missing.
You might want to check:
if params[:cat].blank?
# ...
end
if params[:subcat].blank?
# ...
end
I would also use blank? instead of empty? (checking for both nil and "").
I experienced a similar issue when working on a Rails 6 application.
I had 2 models that were connected by a one-to-one relationship:
Admin Model
class Admin < ApplicationRecord
has_one :personal_info, dependent: :destroy
accepts_nested_attributes_for :personal_info, update_only: true, allow_destroy:
end
Personal Info Model
class PersonalInfo < ApplicationRecord
before_save :personal_info_capitalize
# This is an enum that can be found in app/models/concerns
include Gender
belongs_to :admin, optional: true
private
def personal_info_capitalize
# set the personal info details to capitalize letters
self.first_name = first_name.capitalize
self.last_name = last_name.capitalize
end
end
And for the Admin Registration and Sessions, I was using the Devise gem
AdminsController
class Admins::RegistrationsController < Devise::RegistrationsController
# before_action :configure_sign_up_params, only: [:create]
.
.
.
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up, keys:
[personal_info_attributes: [:first_name,
:last_name]])
end
end
However, when I try to create a new Admin, it throws the error:
NoMethodError (undefined method `capitalize' for nil:NilClass):
And when I look at my logs, I see the following:
Started POST "/admins" for 127.0.0.1 at 2020-08-20 12:09:19 +0100
Processing by Admins::RegistrationsController#create as HTML
Parameters: {"authenticity_token"=>"+RcJcpyO7mh+DmWOJ0IsymbuIascduIamDI+8RcUxWukL5B+aNAlKlgbqNzanW1EZIJ5WCs3bmV9OI+PLRI43g==", "admin"=>{"personal_info_attributes"=>{"first_name"=>"Promise", "last_name"=>"Chukwuenyem"}, "email"=>"admin#gmail.com", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "commit"=>"Sign Up"}
Unpermitted parameter: :personal_info_attributes
Here's how I fixed it:
The issue was that the callback that will invoke the sign_up parameters was commented out. So the personal_info attributes weren't being permitted by the AdminsController.
I simply uncommented the configure_sign_up_params before_action in the AdminsController in other to permit those parameters:
class Admins::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create]
.
.
.
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up, keys:
[personal_info_attributes: [:first_name,
:last_name]])
end
end
That's all.
I hope this helps

Rails Authlogic authentication method

Within Authlogic, is there a way that I can add conditions to the authentication method? I know by using the find_by_login_method I can specify another method to use, but when I use this I need to pass another parameter since the find_by_login_method method only passes the parameter that is deemed the 'login_field'.
What I need to do is check something that is an association of the authentic model.. Here is the method I want to use
# make sure that the user has access to the subdomain that they are
# attempting to login to, subdomains are company names
def self.find_by_email_and_company(email, company)
user = User.find_by_email(email)
companies = []
user.brands.each do |b|
companies << b.company.id
end
user && companies.include?(company)
end
But this fails due to the fact that only one parameter is sent to the find_by_email_and_company method.
The company is actually the subdomain, so in order to get it here I am just placing it in a hidden field in the form (only way I could think to get it to the model)
Is there a method I can override somehow..?
Using the answer below I came up with the following that worked:
User Model (User.rb)
def self.find_by_email_within_company(email)
# find the user
user = self.find_by_email(email)
# no need to continue if the email address is invalid
return false if user.nil?
# collect the subdomains the provided user has access to
company_subdomains = user.brands.map(&:company).map(&:subdomain)
# verify that the user has access to the current subdomain
company_subdomains.include?(Thread.current[:current_subdomain]) && user
end
Application Controller
before_filter :set_subdomain
private
def set_subdomain
# helper that retreives the current subdomain
get_company
Thread.current[:current_subdomain] = #company.subdomain
end
User Session Model (UserSession.rb)
find_by_login_method :find_by_email_within_company
I have read a few things about using Thread.current, and conflicting namespaces.. This is a great solution that worked for me but would love to hear any other suggestions before the bounty expires, otherwise, +100 to Jens Fahnenbruck :)
Authlogic provides API for dealing with sub domain based authentication.
class User < ActiveRecord::Base
has_many :brands
has_many :companies, :through => :brands
acts_as_authentic
end
class Brand < ActiveRecord::Base
belongs_to :user
belongs_to :company
end
class Company < ActiveRecord::Base
has_many :brands
has_many :users, :through => :brands
authenticates_many :user_sessions, :scope_cookies => true
end
Session controller:
class UserSessionsController < ApplicationController
def create
#company = Company.find(params[:user_session][:company])
#user_session = #company.user_sessions.new(params[:user_session])
if #user_session.save
else
end
end
end
On the other hand
Here is a way to solve the problem using your current approach(I would use the first approach):
Set custom data - to the key email of the hash used to create the UserSession object.
AuthLogic will pass this value to find_by_login method. In the find_by_login method access the needed values.
Assumption:
The sub domain id is set in a field called company in the form.
class UserSessionsController < ApplicationController
def create
attrs = params[:user_session].dup #make a copy
attrs[:email] = params[:user_session] # set custom data to :email key
#user_session = UserSession.new(attrs)
if #user_session.save
else
end
end
end
Model code
Your code for finding the user with the given email and subdomain can be simplified and optimized as follows:
class User < ActiveRecord::Base
def find_by_email params={}
# If invoked in the normal fashion then ..
return User.first(:conditions => {:email => params}) unless params.is_a?(Hash)
User.first(:joins => [:brands => :company}],
:conditions => ["users.email = ? AND companies.id = ?",
params[:email], params[:company]])
end
end
Edit 1
Once the user is authenticated, system should provide access to authorized data.
If you maintain data for all the domains in the same table, then you have to scope the data by subdomain and authenticated user.
Lets say you have Post model with company_id and user_id columns. When a user logs in you want to show user's posts for the sub domain. This is one way to scope user's data for the subdomain:
Posts.find_by_company_id_and_user_id(current_company, current_user)
Posts.for_company_and_user(current_company, current_user) # named scope
If you do not scope the data, you will have potential security holes in your system.
In your lib folder add a file with the follwing content:
class Class
def thread_local_accessor name, options = {}
m = Module.new
m.module_eval do
class_variable_set :"###{name}", Hash.new {|h,k| h[k] = options[:default] }
end
m.module_eval %{
FINALIZER = lambda {|id| ###{name}.delete id }
def #{name}
###{name}[Thread.current.object_id]
end
def #{name}=(val)
ObjectSpace.define_finalizer Thread.current, FINALIZER unless ###{name}.has_key? Thread.current.object_id
###{name}[Thread.current.object_id] = val
end
}
class_eval do
include m
extend m
end
end
end
I found this here
Then add code in the controller like this:
class ApplicationController < ActionController
before_filter :set_subdomain
private
def set_subdomain
User.subdomain = request.subdomains[0]
end
end
And now you can do the following in your user model (assuming your company model has a method called subdomain:
class User < ActiveRecord::Base
thread_local_accessor :subdomain, :default => nil
def self.find_by_email_within_company(email)
self.find_by_email(email)
company_subdomains = user.brands.map(&:company).map(&:subdomain)
company_subdomains.include?(self.subdomain) && user
end
end
And FYI:
companies = user.brands.map(&:company).map(&:subdomain)
is the same as
companies = []
user.brands.each do |b|
companies << b.company.subdomain
end
With rails 3 you can use this workaround:
class UserSessionsController < ApplicationController
...
def create
#company = <# YourMethodToGetIt #>
session_hash = params[:user_session].dup
session_hash[:username] = { :login => params[:user_session][:username], :company => #company }
#user_session = UserSession.new(session_hash)
if #user_session.save
flash[:notice] = "Login successful!"
redirect_back_or_default dashboard_url
else
#user_session.username = params[:user_session][:username]
render :action => :new
end
...
end
Then
class UserSession < Authlogic::Session::Base
find_by_login_method :find_by_custom_login
end
and
class User < ActiveRecord::Base
...
def self.find_by_custom_login(hash)
if hash.is_a? Hash
return find_by_username_and_company_id(hash[:login], hash[:company].id) ||
find_by_email_and_company_id(hash[:login], hash[:company].id)
else
raise Exception.new "Error. find_by_custom_login MUST be called with {:login => 'username', :company => <Company.object>}"
end
end
...
end
Which is quite plain and "correct". I take me a lot of time to find out, but it works fine!

Resources