Active Record 4.x and MySQL table column type without using migrations - mysql

I'm doing some test with Sinatra v1.4.4 and Active Record v4.0.2. I've created a DBase and a table named Company with Mysql Workbench. In table Company there are two fields lat & long of DECIMAL(10,8) and DECIMAL(11,8) type respectively. Without using migrations I defined the Company model as follow:
class Company < ActiveRecord::Base
end
Everything works except the fact that lat and lng are served as string and not as float/decimal. Is there any way to define the type in the above Class Company definition. Here you can find the Sinatra route serving the JSON response:
get '/companies/:companyId' do |companyId|
begin
gotCompany = Company.find(companyId)
[200, {'Content-Type' => 'application/json'}, [{code:200, company: gotCompany.attributes, message: t.company.found}.to_json]]
rescue
[404, {'Content-Type' => 'application/json'}, [{code:404, message:t.company.not_found}.to_json]]
end
end
Active Record correctly recognize them as decimal. For example, executing this code:
Company.columns.each {|c| puts c.type}
Maybe its the Active Record object attributes method typecast?
Thanks,
Luca

You can wrap the getter methods for those attributes and cast them:
class Company < ActiveRecord::Base
def lat
read_attribute(:lat).to_f
end
def lng
read_attribute(:lng).to_f
end
end
That will convert them to floats, e.g:
"1.61803399".to_f
=> 1.61803399
Edit:
Want a more declarative way? Just extend ActiveRecord::Base:
# config/initializers/ar_type_casting.rb
class ActiveRecord::Base
def self.cast_attribute(attribute, type_cast)
define_method attribute do
val = read_attribute(attribute)
val.respond_to?(type_cast) ? val.send(type_cast) : val
end
end
end
Then use it like this:
class Company < ActiveRecord::Base
cast_attribute :lat, :to_f
cast_attribute :lng, :to_f
end
Now when you call those methods on an instance they will be type casted to_f.

Following diego.greyrobot reply I modified my Company class with an additional method. It overrides the attributes method and afterwards typecast the needed fields. Yet something more declarative would be desirable imho.
class Company < ActiveRecord::Base
def attributes
retHash = super
retHash['lat'] = self.lat.to_f
retHash['lng'] = self.lng.to_f
retHash
end
end

Related

Ruby on Rails: Filter list of records in model before returning it to controller?

Lets assume I have the following model:
class Computer < ActiveRecord::Base
end
and I retrieve all computers in my controllers like this:
#computers = Computer.all
Now I add a feature to deactivate certain computers and filter the deactivated computer like this:
class Computer < ActiveRecord::Base
scope :not_deactivated, -> { where('deactivated IS NULL OR deactivated = ?', false) }
end
and in my controllers:
#computers = Computer.all.not_deactivated
The issue with this method is that I have to add not_deactivated in all my controllers.
Is it possible to do this filter in the model so I don't have to touch the controllers?
Easy and common thing to do in controller:
before_action :not_deactivated # , only [:index...]
private
def not_deactivated
#computers = Computer.where(your code)
end
Since the controller handles the view you must initiate the object somehow anyway. By filtering like that you can achieve what you are trying to do now with the model filter.
you can do it by declaring default_scope to run this every time you make a call to your Computer model
class Computer < ActiveRecord::Base
default_scope { where('deactivated IS NULL OR deactivated = ?', false) }
end
So, when you run Computer.all the result you get is Computer.where('deactivated IS NULL OR deactivated = ?', false)

How do you Skip an Object in an ActiveModel Serializer Array?

I have searched through all the active model serializer (v 0.9.0) documentation and SO questions I can find, but can't figure this out.
I have objects which can be marked as "published" or "draft". When they aren't published, only the user who created the object should be able to see it. I can obviously set permissions for "show" in my controller, but I also want to remove these objects from the json my "index" action returns unless it is the correct user. Is there a way to remove this object from the json returned by the serializer completely?
In my activemodel serializer, I am able to user filter(keys) and overloaded attributes to remove the data, as shown using my code below, but I can't just delete the entire object (I'm left having to return an empty {} in my json, trying to return nil breaks the serializer).
I'm probably missing something simple. Any help would be much appreciated!
class CompleteExampleSerializer < ExampleSerializer
attributes :id, :title
has_many :children
def attributes
data = super
(object.published? || object.user == scope || scope.admin?) ? data : {}
end
def filter(keys)
keys = super
(object.published? || object.user == scope || scope.admin?) ? keys : {}
end
end
That looks correct, try returning an array instead of a hash when you dont want any keys. Also, I don't think calling super is necessary b/c the filter takes in the keys.
Also, I don't think defining an attributes method is necessary.
I have chapters that can either be published or unpublished. They're owned by a story so I ended doing something like below.
has_many :unpublished_chapters, -> { where published: false }, :class_name => "Chapter", dependent: :destroy
has_many :published_chapters, -> { where published: true }, :class_name => "Chapter", dependent: :destroy
Inside of my serializer, I choose to include unpublished_chapters only if the current_user is the owner of those chapters. In ams 0.8.0 the syntax is like so.
def include_associations!
include! :published_chapters if ::Authorization::Story.include_published_chapters?(current_user,object,#options)
include! :unpublished_chapters if ::Authorization::Story.include_unpublished_chapters?(current_user,object,#options)
end
In my case, it's not so bad to differentiate the two and it saves me the trouble of dealing with it on the client. Our situations are similar but say you want to get all of the chapters by visiting the chapters index route. This doesn't make much sense in my app but you could go to that controller and render a query on that table.

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.

accessing the values from a belongs_to table

I am trying to get the values from a belongs_to table in my controller (so i can use these values to insert into another model.
class AccesspointsController < ApplicationController
def create
#accesspoint = Accesspoint.new(params[:accesspoint])
#accesspoint.build_radgroupcheck(:value => params[:accesspoint][:shortname], :groupname => params[:user][:email])
end
end
the other model is radgroupcheck while i am trying to get the email data from the user table. and copy insert it over to radgroupcheck. the other params are ok (accesspoint and shortname to the same radgroupcheck table)
while doing :groupname => params[:user][:email] - i get a nil (because the params are not from the controller i am using though i am using this controller to add everything)
How do i go about getting the params from another model (with the user already registered with the email) - the accesspoint model belongs_to the user model.

Ruby libxml parsing and inserting to database

I am currently trying to read from an xml file which records the jobs on a PBS. I have succesfullly managed to parse the code, but am unable to insert the objtects into my database, i receive this error:
"You have a nil object when you didn't expect it!
You might have expected an instance of ActiveRecord::Base.
The error occurred while evaluating nil.delete"
This is my Model:
require 'xml/libxml'
class Job < ActiveRecord::Base
JOB_DIR = File.join('data', 'jobs')
attr_reader :jobid, :user, :group, :jobname, :queue, :ctime
attr_reader :qtime, :etime, :start, :owner
def initialize(jobid, user, group, jobname, queue, ctime, qtime, etime, start, owner)
#jobid, #user, #group, #jobname, #queue = jobid, user, group, jobname, queue
#ctime, #qtime, #etime, #start, #owner = ctime, qtime, etime, start, owner
end
def self.find_all()
jobs = []
input_file = "#{JOB_DIR}/1.xml"
doc = XML::Document.file(input_file)
doc.find('//execution_record').each do |node|
jobs << Job.new(
node.find('jobid').to_a.first.content,
node.find('user').to_a.first.content,
node.find('group').to_a.first.content,
node.find('jobname').to_a.first.content,
node.find('queue').to_a.first.content,
node.find('ctime').to_a.first.content,
node.find('qtime').to_a.first.content,
node.find('etime').to_a.first.content,
node.find('start').to_a.first.content,
node.find('owner').to_a.first.content
)
end
jobs
end
end
An my Model Controller:
class JobController < ApplicationController
def index
#jobs = Job.find_all()
end
def create
#jobs = Job.find_all()
for job in #jobs
job.save
end
end
end
I would appreciate any help...Thank you!
I'm not sure on the causes of the error message you're seeing because I can't see anywhere that you're trying to invoke a delete method, however this does seem like a slightly confused use of ActiveRecord.
If you have a jobs database table with fields jobid, user, group, jobname etc. then ActiveRecord will create accessor methods for these and you should not be using attr_reader or overriding initialize. You should also not be setting values value instance variables (#jobid etc.) If you don't have such fields on your table then there is nothing in your current code to map the values from the XML the database fields.
Your def self.find_all method should probably be along the lines of:
def self.build_from_xml
jobs = []
input_file = "#{JOB_DIR}/1.xml"
doc = XML::Document.file(input_file)
doc.find('//execution_record').each do |node|
jobs << Job.new(
:jobid => node.find('jobid').to_a.first.content,
:user => node.find('user').to_a.first.content,
...
Rails used to have a method of its own find_all to retrieve all existing records from the database so your method name is probably a bit misleading. Rails tends to use the build verb to mean create a new model object but don't save it yet so that's why I've gone with a name like build_from_xml.