Removing password_digest from API json? - json

I'm using Sinatra to make a simple little API. I have not been able to figure out a way to remove the 'password_digest' field from the JSON I'm outputting. Well, I know of a long way that I can do it, but I have a feeling there is a much simpler way.
get "/users/all" do
content_type :json
#users = User.all
response = #users.map do |user|
user = user.to_h
user.delete("password_digest")
user
end
response.to_json
end
All I'm trying to do is remove the password_digest field from the output. Is there a simple way to do this? I've tried searching with no luck.

get "/users/all" do
content_type :json
#users = User.all
#users.to_json(except: [:password_digest])
end
You can also overide #as_json on the model to remove the attribute completely from serialization:
class User < ActiveRecord::Base
def as_json(**options)
# this coerces the option into an array and merges the passed
# values with defaults
excluding = [options[:exclude]].flatten
.compact
.union([:password_digest])
super(options.merge(exclude: excluding))
end
end

You should be able to do this:
get "/users/all" do
content_type :json
#users = User.all
response = #users.map do |user|
user = user.to_h # If your data is already a hash, you don't need this line.
user.delete(:password_digest) # <-- If your keys are symbolized
user.delete("password_digest") # <-- If your keys are strings
user
end
response.to_json
end

Related

rails before_action on csv download

I'm using devise to authenticate users. I want them to be able to look at the page without being logged in, but if they wish to download the csv data, they must be logged in. Here's how i've set up the csv download part
respond_to do |format|
format.html{
render :layout => 'indices_show'
}
format.csv{
export_to_csv(idxp)
}
end
and here is the export_to_csv function
def export_to_csv(idxf)
cash = params[:cash]
#title = get_title(#index)
if (cash =='1')
navs = #index.navsc.from(idxf)
r = #index.returnsc.from(idxf)
else
navs = #index.navs.from(idxf)
r = #index.returns.from(idxf)
end
dates = #index.dates.from(idxf)
csv_string = CSV.generate do |csv|
csv << [#title]
csv << ["Date", "Return", "NAV"]
dates.each_with_index do |d, i|
csv << [d,r[i],navs[i]]
end
end
send_data csv_string,
:type => 'text/csv; charset=iso-8859-1; header=present',
:disposition => "attachment; filename ="+ #title +".csv"
end
At the top of this controller, I have,
before_action :authenticate_user!, only:[:export_to_csv]
but it doesn't do anything as the user is still able to download the data without being logged in. I've found a semi work around by doing this,
respond_to do |format|
format.html{
render :layout => 'indices_show'
}
format.csv{
if (user_signed_in?)
export_to_csv(idxp)
else
redirect_to new_user_session_path
end
}
end
The problem here is that once the user logs in, it redirects to the homepage. Is there a way such that when a user clicks the download button, they are forced to sign in, and upon signing in they are redirected back to that page and the data is downloaded? Thanks
The before_action isn't working because export_to_csv is not the method being called as the action - that would normally be the method your routing maps to - i.e. probably the method with your respond_to code in it. Of course, you can split the method into two separate ones and have one for html and one for csv and then have the before_action set up for the csv one. That's perhaps not so nice, especially if it's just two different viewing formats for the same data. If they're not very similar views, perhaps it would be appropriate to separate them. It depends on your app.
Another option is to use your second approach but modify it. Before the redirect_to new_user_session_path, try inserting a call to store_location_for(:user, request.request_uri), or perhaps pass request.original_url. See here for the doc/code:
https://github.com/plataformatec/devise/blob/master/lib/devise/controllers/store_location.rb#L26
You can see that if the stored location is present, it's used in preference to the root path:
https://github.com/plataformatec/devise/blob/master/lib/devise/controllers/helpers.rb#L143
Note that store_location_for seems to be a recent addition to devise. I'm actually using 3.1.x and it doesn't seem to be defined - you'd have to set the user_return_to session variable directly because that's what is checked during signin in 3.1.x.
As Tim said, export_to_csv is not the method being filtered by before_action. It is firing actions based on the controller action methods at the top, and since you are trying to limit access lower down, it has already let the user through.
I'm not sure what the action name is that you are using, so I am going to assume it's show.
You can supply a conditional to your before_action line that may limit what you were hoping for.
before_action :authenticate_user!, only: [:show], if: proc { request.csv? }

Nested strong parameters in rails - AssociationTypeMismatch MYMODEL expected, got ActionController::Parameters()

I'm rendering a model and it's children Books in JSON like so:
{"id":2,"complete":false,"private":false, "books" [{ "id":2,"name":"Some Book"},.....
I then come to update this model by passing the same JSON back to my controller and I get the following error:
ActiveRecord::AssociationTypeMismatch (Book (#2245089560) expected, got ActionController::Parameters(#2153445460))
In my controller I'm using the following to update:
#project.update_attributes!(project_params)
private
def project_params
params.permit(:id, { books: [:id] } )
end
No matter which attributes I whitelist in permit I can't seem to save the child model.
Am I missing something obvious?
Update - another example:
Controller:
def create
#model = Model.new(model_params)
end
def model_params
params.fetch(:model, {}).permit(:child_model => [:name, :other])
end
Request:
post 'api.address/model', :model => { :child_model => { :name => "some name" } }
Model:
accepts_nested_attributes_for :child_model
Error:
expected ChildModel, got ActionController::Parameters
Tried this method to no avail: http://www.rubyexperiments.com/using-strong-parameters-with-nested-forms/
Are you using accepts_nested_attributes_for :books on your project model? If so, instead of "books", the key should be "books_attributes".
def project_params
params.permit(:id, :complete, :false, :private, books_attributes: [:id, :name])
end
I'm using Angular.js & Rails & Rails serializer, and this worked for me:
Model:
has_many :features
accepts_nested_attributes_for :features
ModelSerializer:
has_many :features, root: :features_attributes
Controller:
params.permit features_attributes: [:id, :enabled]
AngularJS:
ng-repeat="feature in model.features_attributes track by feature.id
My solution to this using ember.js was setting the books_attributes mannualy.
In controller:
def project_params
params[:project][:books_attributes] = params[:project][:books_or_whatever_name_relationships_have] if params[:project][:books_or_whatever_name_relationships_have]
params.require(:project).permit(:attr1, :attr2,...., books_attributes: [:book_attr1, :book_attr2, ....])
end
So rails checks and filters the nested attributes as it expected them to come
This worked for me. My parent model was an Artist and the child model was a Url.
class ArtistsController < ApplicationController
def update
artist = Artist.find(params[:id].to_i)
artist.update_attributes(artist_params)
render json: artist
end
private
def artist_params
remap_urls(params.permit(:name, :description, urls: [:id, :url, :title, :_destroy]))
end
def remap_urls(hash)
urls = hash[:urls]
return hash unless urls
hash.reject{|k,v| k == 'urls' }.merge(:urls_attributes => urls)
end
end
class Artist < ActiveRecord::Base
has_many :urls, dependent: :destroy
accepts_nested_attributes_for :urls, allow_destroy: true
end
class Url < ActiveRecord::Base
belongs_to :artist
end
... and in coffeescript (to handle deletions):
#ArtistCtrl = ($scope, $routeParams, $location, API) ->
$scope.destroyUrls = []
$scope.update = (artist) ->
artist.urls.push({id: id, _destroy: true}) for id in $scope.destroyUrls
artist.$update(redirectToShow, artistError)
$scope.deleteURL = (artist,url) ->
artist.urls.splice(artist.urls.indexOf(url),1)
$scope.destroyUrls.push(url.id)
Something is missing from all of the answers, which is the inputs for fields_for in the form.
The form works if you do this:
f.fields_for #model.submodel do ..
However, the form is sent as model[submodel], but that's what causes the error others have mentioned in their answers. If you try to do model.update(model_params), Rails will raise an error that it's expecting a Submodel type.
To fix this, make sure you follow the :name, value format:
f.fields_for :submodel, #model.submodel do ...
Then in the controller, make sure you put _attributes on your params:
def model_params
params.require(:model).permit(submodel_attributes: [:field])
end
Now the save, update, etc. will work fine.
Wasted several days trying to figure out how to use accepts_nested_attributes with Angular, and the issue is always the same: Rails whitelist will not allow the variables into the params hash. I've tried every single different whitelisting syntax that everyone said on SO and other blogs, tried using :inverse, tried using habtm and mas_many_through, tried manually rolling my own solution but that wont work if the whitelist wont allow params through, tried doing what http://guides.rubyonrails.org says about 'Outside the Scope of Strong Parameters', tried removing whitelisting all together which isnt really an option but it causes other problems anyways. Not sure why rails 4 strong parameter whitelisting wont allow arbitrary data thru, thats a huge problem especially if accepts_nested_attributes doesn't work either.... I guess we are left to just create/delete all associations on a separate page/form/controller and look like an idiot making my end users use several forms/pages to do something that should be easily doable on 1 page with 1 form. Ya know, usually I expect Angular to screw me, but this time Angular worked quite well and it was actually Rails 4 that screwed me twice on 1 issue that should be very straightforward.

Paginating a shuffled ActiveRecord query

I am attempting to paginate a shuffled ActiveRecord query. The syntax for doing this using the Kaminari gem is:
#users = Kaminari.paginate_array(User.all.shuffle).page(params[:page]).per(20)
The issue with this is that User.all is re-shuffled on each pagination request, causing duplicate records to be called. Is there any way to prevent this kind of duplication?
You need to pass seed for rand between queries
params[:seed] ||= Random.new_seed
srand params[:seed].to_i
#users = Kaminari.paginate_array(User.all.shuffle).page(params[:page]).per(20)
And in view add params[:seed] to all kaminari links to pages
As KandadaBoggu points out above, retrieving all of the User records from the database is inefficient when you only need 20. I would suggest using MySQL's RAND() function to perform the randomization before you return from the database. You can still pass a seed value to RAND() to make sure the shuffling only happens once per session.
For example:
class User < ActiveRecord::Base
def self.randomized(seed = nil)
seed = seed.to_i rescue 0
order("RAND(#{seed})")
end
end
class UsersController < ApplicationController
before_filter :set_random_seed
def index
#users = User.randomized(session[:seed]).page(params[:page]).per(20)
end
private
def set_random_seed
session[:seed] ||= Random.new_seed
end
end
I don't have a MySQL installation to test against, but this should perform better than your original code.
You can also do this:
class UsersController < ApplicationController
USERS_SEED = 1000 # Or any another not-so-big number
def set_random_seed
session[:seed] ||= Random.rand(USERS_SEED)
end
end
Because Random.new_seed will generate most likely the same result if your data isn't that big.

Ruby on Rails: Decompression (Zlib::Deflate) doesn't work after certain amount of time

I have a need to compress large chunk of text before saving it to the database and decompress it back once client requests it.
The method I am using right now seems to work fine when I insert new records using the Rails console and query for the newly inserted record right away. i.e., I can decompress the compressed description successfully.
But I am not able to decompress the compressed description for any of my other records added prior to this date. It is really confusing for me especially being a beginnner to the ROR world.
I am using MySQL as a database.
See my Model below to better understand it.
require "base64"
class Video < ActiveRecord::Base
before_save :compress_description
def desc
unless description.blank?
return decompress(description)
end
end
private
def compress_description
unless description.blank?
self.description = compress(description)
end
end
def compress(text)
Base64.encode64(Zlib::Deflate.new(nil, -Zlib::MAX_WBITS).deflate(text, Zlib::FINISH))
end
def decompress(text)
Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(Base64.decode64(text))
end
end
Ok it's actually very easy to reproduce your problem. In rails console do the following
Video.create(:description => "This is a test")
Video.last.description
=> "C8nILFYAokSFktTiEgA=\n"
Video.last.desc
=> "This is a test"
Video.last.save #This update corrupts the description
Video.last.desc
=> "C8nILFYAokSFktTiEgA=\n"
The reason the corruption happens is because you are compressing an already compressed string
You should probably modify your class as follows and you should be fine
require 'base64'
class Video < ActiveRecord::Base
before_save :compress_description
after_find :decompress_description
attr_accessor :uncompressed_description
private
def compress_description
unless #uncompressed_description.blank?
self.description = compress(#uncompressed_description)
end
end
def decompress_description
unless description.blank?
#uncompressed_description = decompress(description)
end
end
def compress(text)
Base64.encode64(Zlib::Deflate.new(nil, -Zlib::MAX_WBITS).deflate(text, Zlib::FINISH))
end
def decompress(text)
Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(Base64.decode64(text))
end
end
Now use your class as follows
Video.create(:uncompressed_description => "This is a test")
Video.last.description
=> "C8nILFYAokSFktTiEgA=\n"
Video.last.uncompressed_description
=> "This is a test"
Video.last.save
Video.last.uncompressed_description
=> "This is a test"

How do I create an rspec test that validates a JSON response?

I have a Groups Controller with a method def inbox.
If the user is a group member then inbox returns a JSON object.
If the user is not a member, then inbox should redirect thanks to CanCan permissions.
How do I write an rspec to test these two use cases?
Current spec:
require 'spec_helper'
describe GroupsController do
include Devise::TestHelpers
before (:each) do
#user1 = Factory.create(:user)
#user1.confirm!
sign_in #user1
#group = Factory(:group)
#permission_user_1 = Factory.create(:permission, :user => #user1, :creator_id => #user1.id, :group => #group)
end
describe "GET inbox" do
it "should be successful" do
get inbox_group_path(#group.id), :format => :json
response.should be_success
end
end
end
Routes:
inbox_group GET /groups/:id/inbox(.:format) {:controller=>"groups", :action=>"inbox"}
Routes File:
resources :groups do
member do
get 'vcard', 'inbox'
end
....
end
This is how I do this:
describe "GET index" do
it "returns correct JSON" do
# #groups.should have(2).items
get :index, :format => :json
response.should be_success
body = JSON.parse(response.body)
body.should include('group')
groups = body['group']
groups.should have(2).items
groups.all? {|group| group.key?('customers_count')}.should be_true
groups.any? {|group| group.key?('customer_ids')}.should be_false
end
end
I'm not using cancan, therefore I cannot help with this part.
Sometimes it might be good enough to verify if response contains valid JSON and to show actual response otherwise, here is an example:
it 'responds with JSON' do
expect {
JSON.parse(response.body)
}.to_not raise_error, response.body
end
Try this:
_expected = {:order => order.details}.to_json
response.body.should == _expected
I think the first thing you want to do is to check that the response is of the correct type, i.e. that it has the Content-Type header set to application/json, something along the lines of:
it 'returns JSON' do
expect(response.content_type).to eq(Mime::JSON)
end
Then, depending on your case, you might want to check whether the response can be parsed as JSON, like wik suggested:
it 'responds with JSON' do
expect {
JSON.parse(response.body)
}.to_not raise_error
end
And you could merge the above two into a single test if you feel like two tests for checking JSON response validity are too much.
To assert JSON you can do this too:
ActiveSupport::JSON.decode(response.body).should == ActiveSupport::JSON.decode(
{"error" => " An email address is required "}.to_json
)
This blog gives some more ideas.