Unable to set numeric DataMapper field to blank value - ruby

I can't seem to find any way to successfully set a decimal field to no value from a form, since the form is returning an empty string. Here's a super-simple test case that manually sets the field to ''
require 'dm-core'
require 'dm-mysql-adapter'
require 'dm-migrations'
DataMapper.setup(:default, 'mysql://localhost/test')
class Product
include DataMapper::Resource
property :id, Serial
property :list_price, Decimal
end
DataMapper.finalize
DataMapper.auto_migrate!
p = Product.new
puts p.save #=> true
p = Product.new(:list_price => '')
puts p.save #=> false
p = Product.new(:list_price => nil)
puts p.save #=> true
As you can see, the list_price field will happily save when it's not set, or when it's set to nil. However, when I use a blank string, it won't save at all -- it's seemingly not being typecast.
It seems like I must be missing something obvious here, since this is a pretty basic use-case for an ORM.

You could create your own setter method for the property to check its type. See the section "Over-riding Accessors" on the Datamapper properties documentation page.
Adding:
def list_price=(new_price)
new_price = nil if new_price == ''
super
end
to your Product class will cause any empty string being set as the value of list_price to be converted to nil, and allow the resource to be saved.

Related

Ruby Datamapper: retrieving record using param in url path returns null - sometimes

I'm creating a Sinatra App using Datamapper.
With the following route, I'm attempting to print the record for an id. So localhost:9292/api/1 should return results for id=1
inside
get '/api/:id' do
I tried a couple things with varied results:
thing = Thing.get(params[:id])
thing.to_json
end
outputs 'null', but:
id_param = params[:id]
id_param
end
prints 1 as expected, and:
hardcoded_thing = Thing.get(1)
hardcoded_thing.to_json
end
correctly prints the hardcoded db record with id=1. So I must be losing it..
Any ideas?
Thanks!
For reference, here's my model:
class Thing
include DataMapper::Resource
include BCrypt
property :id, Serial, :key => true
property :created_at, DateTime
property :updated_at, DateTime
property :name, String, :length => 50
property :cafe_topic, Text
end
Try this:
get '/api/:id' do |id|
thing = Thing.get(id)
thing.to_json
end

Freeze a property in DataMapper Model

Consider I have this following model definition, I want a particular property which should be constant from the moment it has been created
class A
property :a1, String, :freeze => true
end
Is there something like this? or may be using callbacks ?
Try the following:
class YourModel
property :a1, String
def a1=(other)
if a1
raise "A1 is allready bound to a value"
end
attribute_set(:a1, other.dup.freeze)
end
end
The initializer internally delegates to normal attribute writers, so when you initialize the attribute via YourModel.new(:a1 => "Your value") you cannot change it with your_instance.a1 = "your value".. But when you create a fresh instance. instance = YourModel.new you can assign once instance.a1 = "Your Value".
If you don't need to assign the constant, then
property :a1, String, :writer => :private
before :create do
attribute_set :a1, 'some value available at creation time'
end
may suffice

data_mapper, attr_accessor, & serialization only serializing properties not attr_accessor attributes

I'm using data_mapper/sinatra and trying to create some attributes with attr_accessor. The following example code:
require 'json'
class Person
include DataMapper::Resource
property :id, Serial
property :first_name, String
attr_accessor :last_name
end
ps = Person.new
ps.first_name = "Mike"
ps.last_name = "Smith"
p ps.to_json
produces this output:
"{\"id\":null,\"first_name\":\"Mike\"}"
Obviously I would like for it to give me both the first and last name attributes. Any ideas on how to get this to work in the way one would expect so that my json has all of the attributes?
Also, feel free to also explain why my expectation (that I'd get all of the attributes) is incorrect. I'm guessing some internal list of attributes isn't getting the attr_accessor instance variables added to it or something. But even so, why?
Datamapper has it’s own serialization library, dm-serializer, that provides a to_json method for any Datamapper resource. If you require Datamapper with require 'data_mapper' in your code, you are using the data_mapper meta-gem that requires dm-serializer as part of it’s set up.
The to_json method provided by dm-serializer only serializes the Datamapper properties of your object (i.e. those you’ve specified with property) and not the “normal” properties (that you’ve defined with attr_accessor). This is why you get id and first_name but not last_name.
In order to avoid using dm-serializer you need to explicitly require those libraries you need, rather than rely on data_mapper. You will need at least dm-core and maybe others.
The “normal” json library doesn’t include any attributes in the default to_json call on an object, it just uses the objects to_s method. So in this case, if you replace require 'data_mapper' with require 'dm-core', you will get something like "\"#<Person:0x000001013a0320>\"".
To create json representations of your own objects you need to create your own to_json method. A simple example would be to just hard code the attributes you want in the json:
def to_json
{:id => id, :first_name => first_name, :last_name => last_name}.to_json
end
You could create a method that looks at the attributes and properties of the object and create the appropriate json from that instead of hardcoding them this way.
Note that if you create your own to_json method you could still call require 'data_mapper', your to_json will replace the one provided by dm-serializer. In fact dm-serializer also adds an as_json method that you could use to create the combined to_json method, e.g.:
def to_json
as_json.merge({:last_name => last_name}).to_json
end
Thanks to Matt I did some digging and found the :method param for dm-serializer's to_json method. Their to_json method was pretty decent and was basically just a wrapper for an as_json helper method so I overwrote it by just adding a few lines:
if options[:include_attributes]
options[:methods] = [] if options[:methods].nil?
options[:methods].concat(model.attributes).uniq!
end
The completed method override looks like:
module DataMapper
module Serializer
def to_json(*args)
options = args.first
options = {} unless options.kind_of?(Hash)
if options[:include_attributes]
options[:methods] = [] if options[:methods].nil?
options[:methods].concat(model.attributes).uniq!
end
result = as_json(options)
# default to making JSON
if options.fetch(:to_json, true)
MultiJson.dump(result)
else
result
end
end
end
end
This works along with an attributes method I added to a base module I use with my models. The relevant section is below:
module Base
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def attr_accessor(*vars)
#attributes ||= []
#attributes.concat vars
super(*vars)
end
def attributes
#attributes || []
end
end
def attributes
self.class.attributes
end
end
now my original example:
require 'json'
class Person
include DataMapper::Resource
include Base
property :id, Serial
property :first_name, String
attr_accessor :last_name
end
ps = Person.new
ps.first_name = "Mike"
ps.last_name = "Smith"
p ps.to_json :include_attributes => true
Works as expected, with the new option parameter.
What I could have done to selectively get the attributes I wanted without having to do the extra work was to just pass the attribute names into the :methods param.
p ps.to_json :methods => [:last_name]
Or, since I already had my Base class:
p ps.to_json :methods => Person.attributes
Now I just need to figure out how I want to support collections.

How do you pass method params in Ruby which can be used as the name of existing methods?

Background: I'm using a DSL for automated UI testing in Ruby called Watir-Webdriver.
I want to write a very re-usable method that passes or fails when a specific HTML element is present. Here is what I have so far:
require 'watir-webdriver'
require 'rspec'
b = Watir::Browser.new
def display_check(element_type,unique_element,expectation)
if expectation == "yes"
b.send(element_type).((:id or :class or :name or :value),/#{Regexp.escape(unique_element)}/).exists?.should == true
else
b.send(element_type).((:id or :class or :name or :value),/#{Regexp.escape(unique_element)}/).exists?.should == false
end
end
I can understand that "div" in this example is a string passed as a method argument. But in the context of the dsl, "div" (minus the quotes) is also a Watir-webdriver method. So I guess I need to somehow convert the string to an eligible watir-webdriver method
I basically want to do the following to determine if an element exists.
display_check("div","captcha","no")
Since I'll be looking for select_lists, divs, radio buttons etc, it would be very useful to specify the element type as an option instead of having it hard coded to the method.
When you use send, the first parameter is the method name and the following parameters are the parameters to pass to the method. See doc.
So your b.send should be more like:
b.send(element_type, :id, /#{Regexp.escape(unique_element)}/).exists?
To find an element where one of the attributes (id, class, etc) is a certain value, you can try the following. Basically it iterates through each of the attributes until an element is found.
def display_check(b, element_type, unique_element, expectation)
element_exists = false
[:id, :class, :name, :value].each do |attribute|
if b.send(element_type, attribute, /#{Regexp.escape(unique_element)}/).exists?
element_exists = true
break
end
end
if expectation == "yes"
element_exists.should == true
else
element_exists.should == false
end
end

Sinatra Variable Scope

Take the following code:
### Dependencies
require 'rubygems'
require 'sinatra'
require 'datamapper'
### Configuration
config = YAML::load(File.read('config.yml'))
name = config['config']['name']
description = config['config']['description']
username = config['config']['username']
password = config['config']['password']
theme = config['config']['theme']
set :public, 'views/themes/#{theme}/static'
### Models
DataMapper.setup(:default, "sqlite3://#{Dir.pwd}/marvin.db")
class Post
include DataMapper::Resource
property :id, Serial
property :name, String
property :body, Text
property :created_at, DateTime
property :slug, String
end
class Page
include DataMapper::Resource
property :id, Serial
property :name, String
property :body, Text
property :slug, String
end
DataMapper.auto_migrate!
### Controllers
get '/' do
#posts = Post.get(:order => [ :id_desc ])
haml :"themes/#{theme}/index"
end
get '/:year/:month/:day/:slug' do
year = params[:year]
month = params[:month]
day = params[:day]
slug = params[:slug]
haml :"themes/#{theme}/post.haml"
end
get '/:slug' do
haml :"themes/#{theme}/page.haml"
end
get '/admin' do
haml :"admin/index.haml"
end
I want to make name, and all those variables available to the entire script, as well as the views. I tried making them global variables, but no dice.
Might not be the "cleanest" way to do it, but setting them as options should work:
--> http://www.sinatrarb.com/configuration.html :)
setting:
set :foo, 'bar'
getting:
"foo is set to " + settings.foo
Make them constants. They should be anyway shouldn't they? They're not going to change.
Make a constant by writing it in all caps.
Read this article on Ruby Variable Scopes if you have any more issues.
http://www.techotopia.com/index.php/Ruby_Variable_Scope
Another clean option may be a config class, where the init method loads the YAML and then sets up the variables.
Have fun. #reply me when you've finished your new blog (I'm guessing this is what this is for).
From the Sinatra README:
Accessing Variables in Templates
Templates are evaluated within the same context as route handlers. Instance variables set in route handlers are direcly accessible by templates:
get '/:id' do
#foo = Foo.find(params[:id])
haml '%h1= #foo.name'
end
Or, specify an explicit Hash of local variables:
get '/:id' do
foo = Foo.find(params[:id])
haml '%h1= foo.name', :locals => { :foo => foo }
end
This is typically used when rendering templates as partials from within other templates.
A third option would be to set up accessors for them as helper methods. (Which are also available throughout the application and views.)
what also works:
##foo = "bar"
But don't forget to restart the server after this change

Resources