adding rdoc to method properties_with_values

This commit is contained in:
Lucas Renan 2010-12-22 23:09:00 -02:00
commit 92a10dbfc9
30 changed files with 764 additions and 506 deletions

162
README.md
View file

@ -9,16 +9,13 @@ for validations and callbacks.
If your project is still running Rails 2.3, you'll have to continue using ExtendedDocument as
it is not possible to load ActiveModel into programs that do not use ActiveSupport 3.0.
CouchRest Model only supports CouchDB 0.10.0 or newer.
CouchRest Model is only tested on CouchDB 1.0.0 or newer.
## Install
### From Gem
### Gem
CouchRest Model depends on Rails 3's ActiveModel which has not yet been released. You'll need to add
`--pre` to the end of the gem install until the dependencies are stable:
$ sudo gem install couchrest_model --pre
$ sudo gem install couchrest_model
### Bundler
@ -71,16 +68,14 @@ but no guarantees!
## Properties
Only attributes with a property definition will be stored be CouchRest Model (as opposed
to a normal CouchRest Document which will store everything). To help prevent confusion,
a property should be considered as the definition of an attribute. An attribute must be associated
with a property, but a property may not have any attributes associated if none have been set.
A property is the definition of an attribute, it describes what the attribute is called, how it should
be type casted and other options such as the default value. These replace your typical
`add_column` methods found in relational database migrations.
Attributes with a property definition will have setter and getter methods defined for them. Any other attibute
can be set in the same way you'd update a Hash, this funcionality is inherited from CouchRest Documents.
In its simplest form, a property
will only create a getter and setter passing all attribute data directly to the database. Assuming the attribute
provided responds to `to_json`, there will not be any problems saving it, but when loading the
data back it will either be a string, number, array, or hash:
Here are a few examples of the way properties are used:
class Cat < CouchRest::Model::Base
property :name
@ -105,8 +100,7 @@ Properties create getters and setters similar to the following:
write_attribute('name', value)
end
Properties can also have a type which
will be used for casting data retrieved from CouchDB when the attribute is set:
Properties can also have a type which will be used for casting data retrieved from CouchDB when the attribute is set:
class Cat < CouchRest::Model::Base
property :name, String
@ -120,7 +114,7 @@ will be used for casting data retrieved from CouchDB when the attribute is set:
@cat.last_fed_at < 20.minutes.ago # True!
Booleans or TrueClass will also create a getter with question mark at the end:
Boolean or TrueClass types will create a getter with question mark at the end:
class Cat < CouchRest::Model::Base
property :awake, TrueClass, :default => true
@ -130,9 +124,8 @@ Booleans or TrueClass will also create a getter with question mark at the end:
Adding the +:default+ option will ensure the attribute always has a value.
Defining a property as read-only will mean that its value is set only when read from the
database and that it will not have a setter method. You can however update a read-only
attribute using the `write_attribute` method:
A read-only property will only have a getter method, and its value is set when the document
is read from the database. You can however update a read-only attribute using the `write_attribute` method:
class Cat < CouchRest::Model::Base
property :name, String
@ -148,10 +141,23 @@ attribute using the `write_attribute` method:
@cat.fall_off_balcony!
@cat.lives # Now 8!
Mass assigning attributes is also possible in a similar fashion to ActiveRecord:
@cat.attributes = { :name => "Felix" }
@cat.save
Is the same as:
@cat.update_attributes(:name => "Felix")
By default, attributes without a property will not be updated via the `#attributes=` method. This provents useless data being passed to database, for example from an HTML form. However, if you would like truely
dynamic attributes, the `mass_assign_any_attribute` configuration option when set to true will
store everything you put into the `Base#attributes=` method.
## Property Arrays
An attribute may also contain an array of data. CouchRest Model handles this, along
An attribute may contain an array of data. CouchRest Model handles this, along
with casting, by defining the class of the child attributes inside an Array:
class Cat < CouchRest::Model::Base
@ -193,15 +199,14 @@ you'd like to use. For example:
@cat.toys.first.class == CatToy
@cat.toys.first.name == 'mouse'
Additionally, any hashes sent to the property will automatically be converted:
Any hashes sent to the property will automatically be converted:
@cat.toys << {:name => 'catnip ball'}
@cat.toys.last.is_a?(CatToy) # True!
Of course, to use your own classes they *must* be defined before the parent uses them otherwise
To use your own classes they *must* be defined before the parent uses them otherwise
Ruby will bring up a missing constant error. To avoid this, or if you have a really simple array of data
you'd like to model, the latest version of CouchRest Model (> 1.0.0) supports creating
anonymous classes:
you'd like to model, CouchRest Model supports creating anonymous classes:
class Cat < CouchRest::Model::Base
property :name, String
@ -216,7 +221,7 @@ anonymous classes:
@cat.toys.last.rating == 5
@cat.toys.last.name == 'catnip ball'
Using this method of anonymous classes will *only* create arrays of objects.
Anonymous classes will *only* create arrays of objects.
## Assocations
@ -227,14 +232,67 @@ Two types at the moment:
collection_of :tags
TODO: Document properly!
This is a somewhat controvesial feature of CouchRest Model that some document database purists may fringe at. CouchDB does not yet povide many features to support relationships between documents but the fact of that matter is that its a very useful paradigm for modelling data systems.
In the near future we hope to add support for a `has_many` relationship that takes of the _Linked Documents_ feature that arrived in CouchDB 0.11.
### Belongs To
Creates a property in the document with `_id` added to the end of the name of the foreign model with getter and setter methods to access the model.
Example:
class Cat < CouchRest::Model::Base
belongs_to :mother
property :name
end
kitty = Cat.new(:name => "Felix")
kitty.mother = Mother.find_by_name('Sophie')
Providing a object to the setter, `mother` in the example will automagically update the `mother_id` attribute. Retrieving the data later is just as expected:
kitty = Cat.find_by_name "Felix"
kitty.mother.name == 'Sophie'
Belongs_to accepts a few options to add a bit more felxibility:
* `:class_name` - the camel case string name of the class used to load the model.
* `:foreign_key` - the name of the property to use instead of the attribute name with `_id` on the end.
* `:proxy` - a string that when evaluated provides a proxy model that responds to `#get`.
The last option, `:proxy` is a feature currently in testing that allows objects to be loaded from a proxy class, such as `ClassProxy`. For example:
class Invoice < CouchRest::Model::Base
attr_accessor :company
belongs_to :project, :proxy => 'self.company.projects'
end
A project instance in this scenario would need to be loaded by calling `#get(project_id)` on `self.company.projects` in the scope of an instance of the Invoice. We hope to document and work on this powerful feature in the near future.
### Collection Of
A collection_of relationship is much like belongs_to except that rather than just one foreign key, an array of foreign keys can be stored. This is one of the great features of a document database. This relationship uses a proxy object to automatically update two arrays; one containing the objects being used, and a second with the foreign keys used to the find them.
The best example of this in use is with Labels:
class Invoice < CouchRest::Model::Base
collection_of :labels
end
invoice = Invoice.new
invoice.labels << Label.get('xyz')
invoice.labels << Label.get('abc')
invoice.labels.map{|l| l.name} # produces ['xyz', 'abc']
See the belongs_to relationship for the options that can be used. Note that this isn't especially efficient, a `get` is performed for each model in the array. As with a has_many relationship, we hope to be able to take advantage of the Linked Documents feature to avoid multiple requests.
## Validations
CouchRest Model automatically includes the new ActiveModel validations, so they should work just as the traditional Rails
validations. For more details, please see the ActiveModel::Validations documentation.
CouchRest Model automatically includes the new ActiveModel validations, so they should work just as the traditional Rails validations. For more details, please see the ActiveModel::Validations documentation.
CouchRest Model adds the possibility to check the uniqueness of attributes using the `validates_uniqueness_of` class method, for example:
@ -251,9 +309,7 @@ you'd like to avoid the typical RestClient Conflict error:
unique_id :code
validates_uniqueness_of :code, :view => 'all'
Given that the uniqueness check performs a request to the database, it is also possible
to include a @:proxy@ parameter. This allows you to
call a method on the document and provide an alternate proxy object.
Given that the uniqueness check performs a request to the database, it is also possible to include a `:proxy` parameter. This allows you to call a method on the document and provide an alternate proxy object.
Examples:
@ -264,8 +320,7 @@ Examples:
validates_uniqueness_of :title, :proxy => 'company.people'
A really interesting use of +:proxy+ and +:view+ together could be where
you'd like to ensure the ID is unique between several types of document. For example:
A really interesting use of `:proxy` and `:view` together could be where you'd like to ensure the ID is unique between several types of document. For example:
class Product < CouchRest::Model::Base
property :code
@ -290,26 +345,41 @@ you'd like to ensure the ID is unique between several types of document. For exa
Pretty cool!
## Configuration
CouchRest Model supports a few configuration options. These can be set either for the whole Model code
base or for a specific model of your chosing. To configure globally, provide something similar to the
following in your projects loading code:
CouchRestModel::Model::Base.configure do |config|
config.mass_assign_any_attribute = true
config.model_type_key = 'couchrest-type'
end
To set for a specific model:
class Cat < CouchRest::Model::Base
mass_assign_any_attribute true
end
Options currently avilable are:
* `mass_assign_any_attribute` - false by default, when true any attribute may be updated via the update_attributes or attributes= methods.
* `model_type_key` - 'couchrest-type' by default, is the name of property that holds the class name of each CouchRest Model.
## Notable Issues
CouchRest Model uses active_support for some of its internals. Ensure you have a stable active support gem installed
or at least 3.0.0.beta4.
None at the moment...
JSON gem versions 1.4.X are kown to cause problems with stack overflows and general badness. Version 1.2.4 appears to work fine.
## Ruby on Rails
CouchRest Model is compatible with rails and provides some ActiveRecord-like methods.
The CouchRest companion rails project
[http://github.com/hpoydar/couchrest-rails](http://github.com/hpoydar/couchrest-rails) is great
for provided default connection details for your database. At the time of writting however it
does not provide explicit support for CouchRest Model.
The CouchRest companion rails project [http://github.com/hpoydar/couchrest-rails](http://github.com/hpoydar/couchrest-rails) is great for providing default connection details for your database. At the time of writting however it does not provide explicit support for CouchRest Model.
CouchRest Model and the original CouchRest ExtendedDocument do not share the same namespace,
as such you should not have any problems using them both at the same time. This might
help with migrations.
CouchRest Model and the original CouchRest ExtendedDocument do not share the same namespace, as such you should not have any problems using them both at the same time. This might help with migrations.
### Rails 3.0
@ -321,9 +391,7 @@ In your Gemfile require the gem with a simple line:
## Testing
The most complete documentation is the spec/ directory. To validate your
CouchRest install, from the project root directory run `rake`, or `autotest`
(requires RSpec and optionally ZenTest for autotest support).
The most complete documentation is the spec/ directory. To validate your CouchRest install, from the project root directory run `rake`, or `autotest` (requires RSpec and optionally ZenTest for autotest support).
## Docs

View file

@ -27,7 +27,7 @@ begin
gemspec.extra_rdoc_files = %w( README.md LICENSE THANKS.md )
gemspec.files = %w( LICENSE README.md Rakefile THANKS.md history.txt couchrest.gemspec) + Dir["{examples,lib,spec}/**/*"] - Dir["spec/tmp"]
gemspec.has_rdoc = true
gemspec.add_dependency("couchrest", "~> 1.0.0")
gemspec.add_dependency("couchrest", "~> 1.0.1")
gemspec.add_dependency("mime-types", "~> 1.15")
gemspec.add_dependency("activemodel", "~> 3.0.0.rc")
gemspec.add_dependency("tzinfo", "~> 0.3.22")

View file

@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
s.authors = ["J. Chris Anderson", "Matt Aimonetti", "Marcos Tapajos", "Will Leinweber", "Sam Lown"]
s.date = %q{2010-08-24}
s.date = %q{2010-10-23}
s.description = %q{CouchRest Model provides aditional features to the standard CouchRest Document class such as properties, view designs, associations, callbacks, typecasting and validations.}
s.email = %q{jchris@apache.org}
s.extra_rdoc_files = [
@ -25,14 +25,13 @@ Gem::Specification.new do |s|
"history.txt",
"lib/couchrest/model.rb",
"lib/couchrest/model/associations.rb",
"lib/couchrest/model/attribute_protection.rb",
"lib/couchrest/model/attributes.rb",
"lib/couchrest/model/base.rb",
"lib/couchrest/model/callbacks.rb",
"lib/couchrest/model/casted_array.rb",
"lib/couchrest/model/casted_model.rb",
"lib/couchrest/model/class_proxy.rb",
"lib/couchrest/model/collection.rb",
"lib/couchrest/model/configuration.rb",
"lib/couchrest/model/design_doc.rb",
"lib/couchrest/model/document_queries.rb",
"lib/couchrest/model/errors.rb",
@ -40,6 +39,7 @@ Gem::Specification.new do |s|
"lib/couchrest/model/persistence.rb",
"lib/couchrest/model/properties.rb",
"lib/couchrest/model/property.rb",
"lib/couchrest/model/property_protection.rb",
"lib/couchrest/model/support/couchrest.rb",
"lib/couchrest/model/support/hash.rb",
"lib/couchrest/model/typecast.rb",
@ -55,13 +55,14 @@ Gem::Specification.new do |s|
"lib/rails/generators/couchrest_model/model/templates/model.rb",
"spec/couchrest/assocations_spec.rb",
"spec/couchrest/attachment_spec.rb",
"spec/couchrest/attribute_protection_spec.rb",
"spec/couchrest/base_spec.rb",
"spec/couchrest/casted_model_spec.rb",
"spec/couchrest/casted_spec.rb",
"spec/couchrest/class_proxy_spec.rb",
"spec/couchrest/configuration_spec.rb",
"spec/couchrest/inherited_spec.rb",
"spec/couchrest/persistence_spec.rb",
"spec/couchrest/property_protection_spec.rb",
"spec/couchrest/property_spec.rb",
"spec/couchrest/subclass_spec.rb",
"spec/couchrest/validations.rb",
@ -93,16 +94,17 @@ Gem::Specification.new do |s|
s.homepage = %q{http://github.com/couchrest/couchrest_model}
s.rdoc_options = ["--charset=UTF-8"]
s.require_paths = ["lib"]
s.rubygems_version = %q{1.3.6}
s.rubygems_version = %q{1.3.7}
s.summary = %q{Extends the CouchRest Document for advanced modelling.}
s.test_files = [
"spec/spec_helper.rb",
"spec/couchrest/configuration_spec.rb",
"spec/couchrest/property_spec.rb",
"spec/couchrest/property_protection_spec.rb",
"spec/couchrest/casted_spec.rb",
"spec/couchrest/subclass_spec.rb",
"spec/couchrest/persistence_spec.rb",
"spec/couchrest/casted_model_spec.rb",
"spec/couchrest/attribute_protection_spec.rb",
"spec/couchrest/assocations_spec.rb",
"spec/couchrest/validations.rb",
"spec/couchrest/view_spec.rb",
@ -130,15 +132,15 @@ Gem::Specification.new do |s|
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
s.specification_version = 3
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
s.add_runtime_dependency(%q<couchrest>, ["~> 1.0.0"])
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
s.add_runtime_dependency(%q<couchrest>, ["~> 1.0.1"])
s.add_runtime_dependency(%q<mime-types>, ["~> 1.15"])
s.add_runtime_dependency(%q<activemodel>, ["~> 3.0.0.rc"])
s.add_runtime_dependency(%q<tzinfo>, ["~> 0.3.22"])
s.add_runtime_dependency(%q<railties>, ["~> 3.0.0.rc"])
s.add_development_dependency(%q<rspec>, ["~> 2.0.0.beta.19"])
else
s.add_dependency(%q<couchrest>, ["~> 1.0.0"])
s.add_dependency(%q<couchrest>, ["~> 1.0.1"])
s.add_dependency(%q<mime-types>, ["~> 1.15"])
s.add_dependency(%q<activemodel>, ["~> 3.0.0.rc"])
s.add_dependency(%q<tzinfo>, ["~> 0.3.22"])
@ -146,7 +148,7 @@ Gem::Specification.new do |s|
s.add_dependency(%q<rspec>, ["~> 2.0.0.beta.19"])
end
else
s.add_dependency(%q<couchrest>, ["~> 1.0.0"])
s.add_dependency(%q<couchrest>, ["~> 1.0.1"])
s.add_dependency(%q<mime-types>, ["~> 1.15"])
s.add_dependency(%q<activemodel>, ["~> 3.0.0.rc"])
s.add_dependency(%q<tzinfo>, ["~> 0.3.22"])

View file

@ -1,11 +1,21 @@
== Next Version
* Major enhancements
* Dirty Tracking via ActiveModel
* ActiveModel Attribute Methods support
* Support for configuration module and "model_type_key" option for overriding model's type key
* Added "mass_assign_any_attribute" configuration option to allow setting anything via the attribute= method.
* Minor enhancements
* Fixing find("") issue (thanks epochwolf)
* Altered protected attributes so that hash provided to #attributes= is not modified
* Altering typecasting for floats to better handle commas and points
* Fixing the lame pagination bug where database url (and pass!!) were included in view requests (Thanks James Hayton)
Notes:
* 2010-10-22 @samlown:
* ActiveModel Attribute support was added but has now been removed due to major performance issues.
Until these have been resolved (if possible?!) they should not be included. See the
'active_model_attrs' if you'd like to test.
== CouchRest Model 1.0.0.beta8

View file

@ -1,132 +0,0 @@
module CouchRest
module Model
ReadOnlyPropertyError = Class.new(StandardError)
# Attributes Suffixes provide methods from ActiveModel
# to hook into. See methods such as #attribute= and
# #attribute? for their implementation
AttributeMethodSuffixes = ['', '=', '?']
module Attributes
extend ActiveSupport::Concern
included do
include ActiveModel::AttributeMethods
attribute_method_suffix *AttributeMethodSuffixes
end
module ClassMethods
def attributes
properties.map {|prop| prop.name}
end
end
def initialize(*args)
self.class.attribute_method_suffix *AttributeMethodSuffixes
super
end
def attributes
self.class.attributes
end
## Reads the attribute value.
# Assuming you have a property :title this would be called
# by `model_instance.title`
def attribute(name)
read_attribute(name)
end
## Sets the attribute value.
# Assuming you have a property :title this would be called
# by `model_instance.title = 'hello'`
def attribute=(name, value)
raise ReadOnlyPropertyError, 'read only property' if find_property!(name).read_only
write_attribute(name, value)
end
## Tests for both presence and truthiness of the attribute.
# Assuming you have a property :title # this would be called
# by `model_instance.title?`
def attribute?(name)
value = read_attribute(name)
!(value.nil? || value == false)
end
## Support for handling attributes
#
# This would be better in the properties file, but due to scoping issues
# this is not yet possible.
def prepare_all_attributes(doc = {}, options = {})
apply_all_property_defaults
if options[:directly_set_attributes]
directly_set_read_only_attributes(doc)
else
remove_protected_attributes(doc)
end
directly_set_attributes(doc) unless doc.nil?
end
# Takes a hash as argument, and applies the values by using writer methods
# for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
# missing. In case of error, no attributes are changed.
def update_attributes_without_saving(hash)
# Remove any protected and update all the rest. Any attributes
# which do not have a property will simply be ignored.
attrs = remove_protected_attributes(hash)
directly_set_attributes(attrs)
end
alias :attributes= :update_attributes_without_saving
def read_attribute(property)
prop = find_property!(property)
self[prop.to_s]
end
def write_attribute(property, value)
prop = find_property!(property)
self[prop.to_s] = prop.cast(self, value)
end
private
def read_only_attributes
properties.select { |prop| prop.read_only }.map { |prop| prop.name }
end
def directly_set_attributes(hash)
r_o_a = read_only_attributes
hash.each do |attribute_name, attribute_value|
next if r_o_a.include? attribute_name
if self.respond_to?("#{attribute_name}=")
self.send("#{attribute_name}=", hash.delete(attribute_name))
end
end
end
def directly_set_read_only_attributes(hash)
r_o_a = read_only_attributes
property_list = attributes
hash.each do |attribute_name, attribute_value|
next unless r_o_a.include? attribute_name
if property_list.include?(attribute_name)
write_attribute(attribute_name, hash.delete(attribute_name))
end
end
end
def set_attributes(hash)
attrs = remove_protected_attributes(hash)
directly_set_attributes(attrs)
end
def check_properties_exist(attrs)
property_list = attributes
attrs.each do |attribute_name, attribute_value|
raise NoMethodError, "Property #{attribute_name} not created" unless respond_to?("#{attribute_name}=") or property_list.include?(attribute_name)
end
end
end
end
end

View file

@ -4,6 +4,7 @@ module CouchRest
extend ActiveModel::Naming
include CouchRest::Model::Configuration
include CouchRest::Model::Persistence
include CouchRest::Model::Callbacks
include CouchRest::Model::DocumentQueries
@ -12,11 +13,9 @@ module CouchRest
include CouchRest::Model::ExtendedAttachments
include CouchRest::Model::ClassProxy
include CouchRest::Model::Collection
include CouchRest::Model::AttributeProtection
include CouchRest::Model::Attributes
include CouchRest::Model::PropertyProtection
include CouchRest::Model::Associations
include CouchRest::Model::Validations
include CouchRest::Model::Dirty
def self.subclasses
@subclasses ||= []
@ -48,10 +47,10 @@ module CouchRest
# * :directly_set_attributes: true when data comes directly from database
#
def initialize(doc = {}, options = {})
prepare_all_attributes(doc, options)
doc = prepare_all_attributes(doc, options)
super(doc)
unless self['_id'] && self['_rev']
self['couchrest-type'] = self.class.to_s
self[self.model_type_key] = self.class.to_s
end
after_initialize if respond_to?(:after_initialize)
end

View file

@ -4,10 +4,10 @@ module CouchRest::Model
extend ActiveSupport::Concern
included do
include CouchRest::Model::AttributeProtection
include CouchRest::Model::Attributes
include CouchRest::Model::Configuration
include CouchRest::Model::Callbacks
include CouchRest::Model::Properties
include CouchRest::Model::PropertyProtection
include CouchRest::Model::Associations
include CouchRest::Model::Validations
attr_accessor :casted_by

View file

@ -82,6 +82,7 @@ module CouchRest
design_doc['views'][view_name.to_s] &&
design_doc['views'][view_name.to_s]["couchrest-defaults"]) || {}
view_options = default_view_options.merge(options)
view_options.delete(:database)
[design_doc, view_name, view_options]
end
@ -94,6 +95,8 @@ module CouchRest
raise ArgumentError, 'search_name is required' if search_name.nil?
search_options = options.clone
search_options.delete(:database)
[design_doc, search_name, search_options]
end

View file

@ -0,0 +1,51 @@
module CouchRest
# CouchRest Model Configuration support, stolen from Carrierwave by jnicklas
# http://github.com/jnicklas/carrierwave/blob/master/lib/carrierwave/uploader/configuration.rb
module Model
module Configuration
extend ActiveSupport::Concern
included do
add_config :model_type_key
add_config :mass_assign_any_attribute
configure do |config|
config.model_type_key = 'couchrest-type' # 'model'?
config.mass_assign_any_attribute = false
end
end
module ClassMethods
def add_config(name)
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def self.#{name}(value=nil)
@#{name} = value if value
return @#{name} if self.object_id == #{self.object_id} || defined?(@#{name})
name = superclass.#{name}
return nil if name.nil? && !instance_variable_defined?("@#{name}")
@#{name} = name && !name.is_a?(Module) && !name.is_a?(Symbol) && !name.is_a?(Numeric) && !name.is_a?(TrueClass) && !name.is_a?(FalseClass) ? name.dup : name
end
def self.#{name}=(value)
@#{name} = value
end
def #{name}
self.class.#{name}
end
RUBY
end
def configure
yield self
end
end
end
end
end

View file

@ -31,7 +31,7 @@ module CouchRest
"views" => {
'all' => {
'map' => "function(doc) {
if (doc['couchrest-type'] == '#{self.to_s}') {
if (doc['#{self.model_type_key}'] == '#{self.to_s}') {
emit(doc['_id'],1);
}
}"

View file

@ -1,44 +0,0 @@
# encoding: utf-8
require 'active_model/dirty'
module CouchRest #:nodoc:
module Model #:nodoc:
# Dirty Tracking support via ActiveModel
# mixin methods include:
# #changed?, #changed, #changes, #previous_changes
# #<attribute>_changed?, #<attribute>_change,
# #reset_<attribute>!, #<attribute>_will_change!,
# and #<attribute>_was
#
# Please see the specs or the documentation of
# ActiveModel::Dirty for more information
module Dirty
extend ActiveSupport::Concern
included do
include ActiveModel::Dirty
after_save :clear_changed_attributes
end
def initialize(*args)
super
@changed_attributes.clear if @changed_attributes
end
def write_attribute(name, value)
meth = :"#{name}_will_change!"
__send__ meth if respond_to? meth
super
end
private
def clear_changed_attributes
@previously_changed = changes
@changed_attributes.clear
true
end
end
end
end

View file

@ -8,21 +8,21 @@ module CouchRest
module ClassMethods
# Load all documents that have the "couchrest-type" field equal to the
# Load all documents that have the model_type_key's field equal to the
# name of the current class. Take the standard set of
# CouchRest::Database#view options.
def all(opts = {}, &block)
view(:all, opts, &block)
end
# Returns the number of documents that have the "couchrest-type" field
# Returns the number of documents that have the model_type_key's field
# equal to the name of the current class. Takes the standard set of
# CouchRest::Database#view options
def count(opts = {}, &block)
all({:raw => true, :limit => 0}.merge(opts), &block)['total_rows']
end
# Load the first document that have the "couchrest-type" field equal to
# Load the first document that have the model_type_key's field equal to
# the name of the current class.
#
# ==== Returns

View file

@ -97,7 +97,7 @@ module CouchRest
# ==== Returns
# a document instance
def create_from_database(doc = {})
base = (doc['couchrest-type'].blank? || doc['couchrest-type'] == self.to_s) ? self : doc['couchrest-type'].constantize
base = (doc[model_type_key].blank? || doc[model_type_key] == self.to_s) ? self : doc[model_type_key].constantize
base.new(doc, :directly_set_attributes => true)
end

View file

@ -1,18 +1,13 @@
# encoding: utf-8
require 'set'
module CouchRest
module Model
module Properties
extend ActiveSupport::Concern
class IncludeError < StandardError; end
def self.included(base)
base.class_eval <<-EOS, __FILE__, __LINE__ + 1
included do
extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
self.properties ||= []
EOS
base.extend(ClassMethods)
raise CouchRest::Mixins::Properties::IncludeError, "You can only mixin Properties in a class responding to [] and []=, if you tried to mixin CastedModel, make sure your class inherits from Hash or responds to the proper methods" unless (base.new.respond_to?(:[]) && base.new.respond_to?(:[]=))
raise "You can only mixin Properties in a class responding to [] and []=, if you tried to mixin CastedModel, make sure your class inherits from Hash or responds to the proper methods" unless (method_defined?(:[]) && method_defined?(:[]=))
end
# Returns the Class properties
@ -23,12 +18,45 @@ module CouchRest
self.class.properties
end
# Returns the Class properties with their values
#
# ==== Returns
# Array:: the list of properties with their values
def properties_with_values
props = {}
properties.each { |property| props[property.name] = read_attribute(property.name) }
props
end
# Read the casted value of an attribute defined with a property.
#
# ==== Returns
# Object:: the casted attibutes value.
def read_attribute(property)
self[find_property!(property).to_s]
end
# Store a casted value in the current instance of an attribute defined
# with a property.
def write_attribute(property, value)
prop = find_property!(property)
self[prop.to_s] = prop.is_a?(String) ? value : prop.cast(self, value)
end
# Takes a hash as argument, and applies the values by using writer methods
# for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
# missing. In case of error, no attributes are changed.
def update_attributes_without_saving(hash)
# Remove any protected and update all the rest. Any attributes
# which do not have a property will simply be ignored.
attrs = remove_protected_attributes(hash)
directly_set_attributes(attrs)
end
alias :attributes= :update_attributes_without_saving
private
# The following methods should be accessable by the Model::Base Class, but not by anything else!
def apply_all_property_defaults
return if self.respond_to?(:new?) && (new? == false)
# TODO: cache the default object
@ -37,13 +65,54 @@ module CouchRest
end
end
private
def prepare_all_attributes(doc = {}, options = {})
apply_all_property_defaults
if options[:directly_set_attributes]
directly_set_read_only_attributes(doc)
else
doc = remove_protected_attributes(doc)
end
directly_set_attributes(doc) unless doc.nil?
end
def find_property!(property)
prop = property.is_a?(Property) ? property : self.class.properties.detect {|p| p.to_s == property.to_s}
raise ArgumentError, "Missing property definition for #{property.to_s}" unless prop
raise ArgumentError, "Missing property definition for #{property.to_s}" if prop.nil?
prop
end
# Set all the attributes and return a hash with the attributes
# that have not been accepted.
def directly_set_attributes(hash)
hash.reject do |attribute_name, attribute_value|
if self.respond_to?("#{attribute_name}=")
self.send("#{attribute_name}=", attribute_value)
true
elsif mass_assign_any_attribute # config option
self[attribute_name] = attribute_value
true
else
false
end
end
end
def directly_set_read_only_attributes(hash)
property_list = self.properties.map{|p| p.name}
hash.each do |attribute_name, attribute_value|
next if self.respond_to?("#{attribute_name}=")
if property_list.include?(attribute_name)
write_attribute(attribute_name, hash.delete(attribute_name))
end
end
end
def set_attributes(hash)
attrs = remove_protected_attributes(hash)
directly_set_attributes(attrs)
end
module ClassMethods
def property(name, *options, &block)
@ -91,7 +160,8 @@ module CouchRest
type = [type] # inject as an array
end
property = Property.new(name, type, options)
create_property_alias(property) if property.alias
create_property_getter(property)
create_property_setter(property) unless property.read_only == true
if property.type_class.respond_to?(:validates_casted_model)
validates_casted_model property.name
end
@ -99,15 +169,49 @@ module CouchRest
property
end
def create_property_alias(property)
# defines the getter for the property (and optional aliases)
def create_property_getter(property)
# meth = property.name
class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{property.alias.to_s}
#{property.name}
def #{property.name}
read_attribute('#{property.name}')
end
EOS
if ['boolean', TrueClass.to_s.downcase].include?(property.type.to_s.downcase)
class_eval <<-EOS, __FILE__, __LINE__
def #{property.name}?
value = read_attribute('#{property.name}')
!(value.nil? || value == false)
end
EOS
end
if property.alias
class_eval <<-EOS, __FILE__, __LINE__ + 1
alias #{property.alias.to_sym} #{property.name.to_sym}
EOS
end
end
# defines the setter for the property (and optional aliases)
def create_property_setter(property)
property_name = property.name
class_eval <<-EOS
def #{property_name}=(value)
write_attribute('#{property_name}', value)
end
EOS
if property.alias
class_eval <<-EOS
alias #{property.alias.to_sym}= #{property_name.to_sym}=
EOS
end
end
end # module ClassMethods
end
end
end

View file

@ -58,7 +58,8 @@ module CouchRest::Model
if default.class == Proc
default.call
else
Marshal.load(Marshal.dump(default))
# Marshal.load(Marshal.dump(default)) # Removed as there are no failing tests and caused mutex errors
default
end
end

View file

@ -1,7 +1,9 @@
module CouchRest
module Model
module AttributeProtection
# Attribute protection from mass assignment to CouchRest::Model properties
module PropertyProtection
extend ActiveSupport::Concern
# Property protection from mass assignment to CouchRest::Model properties
#
# Protected methods will be removed from
# * new
@ -20,7 +22,7 @@ module CouchRest
#
# 3) Mix and match, and assume all unspecified properties are protected.
# property :name, :accessible => true
# property :admin, :protected => true
# property :admin, :protected => true # ignored
# property :phone # this will be automatically protected
#
# Note: the timestamps! method protectes the created_at and updated_at properties
@ -32,11 +34,16 @@ module CouchRest
module ClassMethods
def accessible_properties
properties.select { |prop| prop.options[:accessible] }
props = properties.select { |prop| prop.options[:accessible] }
if props.empty?
props = properties.select { |prop| !prop.options[:protected] }
end
props
end
def protected_properties
properties.select { |prop| prop.options[:protected] }
accessibles = accessible_properties
properties.reject { |prop| accessibles.include?(prop) }
end
end
@ -48,28 +55,17 @@ module CouchRest
self.class.protected_properties
end
# Return a new copy of the attributes hash with protected attributes
# removed.
def remove_protected_attributes(attributes)
protected_names = properties_to_remove_from_mass_assignment.map { |prop| prop.name }
return attributes if protected_names.empty?
protected_names = protected_properties.map { |prop| prop.name }
return attributes if protected_names.empty? or attributes.nil?
attributes.reject! do |property_name, property_value|
attributes.reject do |property_name, property_value|
protected_names.include?(property_name.to_s)
end if attributes
attributes || {}
end
end
private
def properties_to_remove_from_mass_assignment
to_remove = protected_properties
unless accessible_properties.empty?
to_remove += properties.reject { |prop| prop.options[:accessible] }
end
to_remove
end
end
end
end

View file

@ -79,7 +79,7 @@ module CouchRest
# Match numeric string
def typecast_to_numeric(value, method)
if value.respond_to?(:to_str)
if value.to_str =~ /\A(-?(?:0|[1-9]\d*)(?:\.\d+)?|(?:\.\d+))\z/
if value.gsub(/,/, '.').gsub(/\.(?!\d*\Z)/, '').to_str =~ /\A(-?(?:0|[1-9]\d*)(?:\.\d+)?|(?:\.\d+))\z/
$1.send(method)
else
value

190
lib/couchrest/model/view.rb Normal file
View file

@ -0,0 +1,190 @@
#### NOTE Work in progress! Not yet used!
module CouchRest
module Model
# A proxy class that allows view queries to be created using
# chained method calls. After each call a new instance of the method
# is created based on the original in a similar fashion to ruby's sequel
# library, or Rails 3's Arel.
#
# CouchDB views have inherent limitations, so joins and filters as used in
# a normal relational database are not possible. At least not yet!
#
#
#
class View
attr_accessor :query, :design, :database, :name
# Initialize a new View object. This method should not be called from outside CouchRest Model.
def initialize(parent, new_query = {}, name = nil)
if parent.is_a? Base
raise "Name must be provided for view to be initialized" if name.nil?
@name = name
@database = parent.database
@query = { :reduce => false }
elsif parent.is_a? View
@database = parent.database
@query = parent.query.dup
else
raise "View cannot be initialized without a parent Model or View"
end
@query.update(new_query)
super
end
# == View Execution Methods
#
# Send a request to the CouchDB database using the current query values.
# Inmediatly send a request to the database for all documents provided by the query.
#
def all(&block)
args = include_docs.query
end
# Inmediatly send a request for the first result of the dataset. This will override
# any limit set in the view previously.
def first(&block)
args = limit(1).include_docs.query
end
def info
end
def offset
end
def total_rows
end
def rows
end
# == View Filter Methods
#
# View filters return an copy of the view instance with the query
# modified appropriatly. Errors will be raised if the methods
# are combined in an incorrect fashion.
#
# Find all entries in the index whose key matches the value provided.
#
# Cannot be used when the +#startkey+ or +#endkey+ have been set.
def key(value)
raise "View#key cannot be used when startkey or endkey have been set" unless query[:startkey].nil? && query[:endkey].nil?
update_query(:key => value)
end
# Find all index keys that start with the value provided. May or may not be used in
# conjunction with the +endkey+ option.
#
# When the +#descending+ option is used (not the default), the start and end keys should
# be reversed.
#
# Cannot be used if the key has been set.
def startkey(value)
raise "View#startkey cannot be used when key has been set" unless query[:key].nil?
update_query(:startkey => value)
end
# The result set should start from the position of the provided document.
# The value may be provided as an object that responds to the +#id+ call
# or a string.
def startkey_doc(value)
update_query(:startkey_docid => value.is_a?(String) ? value : value.id
end
# The opposite of +#startkey+, finds all index entries whose key is before the value specified.
#
# See the +#startkey+ method for more details and the +#inclusive_end+ option.
def endkey(value)
raise "View#endkey cannot be used when key has been set" unless query[:key].nil?
update_query(:endkey => value)
end
# The result set should end at the position of the provided document.
# The value may be provided as an object that responds to the +#id+ call
# or a string.
def endkey_doc(value)
update_query(:endkey_docid => value.is_a?(String) ? value : value.id
end
# The results should be provided in descending order.
#
# Descending is false by default, this method will enable it and cannot be undone.
def descending
update_query(:descending => true)
end
# Limit the result set to the value supplied.
def limit(value)
update_query(:limit => value)
end
# Skip the number of entries in the index specified by value. This would be
# the equivilent of an offset in SQL.
#
# The CouchDB documentation states that the skip option should not be used
# with large data sets as it is inefficient. Use the +startkey_doc+ method
# instead to skip ranges efficiently.
def skip(value = 0)
update_query(:skip => value)
end
# Use the reduce function on the view. If none is available this method will fail.
def reduce
update_query(:reduce => true)
end
# Control whether the reduce function reduces to a set of distinct keys or to a single
# result row.
#
# By default the value is false, and can only be set when the view's +#reduce+ option
# has been set.
def group
raise "View#reduce must have been set before grouping is permitted" unless query[:reduce]
update_query(:group => true)
end
def group_level(value)
raise "View#reduce and View#group must have been set before group_level is called" unless query[:reduce] && query[:group]
update_query(:group_level => value.to_i)
end
protected
def update_query(new_query = {})
self.class.new(self, new_query)
end
# Used internally to ensure that docs are provided. Should not be used outside of
# the view class under normal circumstances.
def include_docs
raise "Documents cannot be returned from a view that is prepared for a reduce" if query[:reduce]
update_query(:include_docs => true)
end
def execute(&block)
end
end
end
end

View file

@ -23,7 +23,7 @@ module CouchRest
# view_by :tags,
# :map =>
# "function(doc) {
# if (doc['couchrest-type'] == 'Post' && doc.tags) {
# if (doc['model'] == 'Post' && doc.tags) {
# doc.tags.forEach(function(tag){
# emit(doc.tag, 1);
# });
@ -39,7 +39,7 @@ module CouchRest
# function:
#
# function(doc) {
# if (doc['couchrest-type'] == 'Post' && doc.date) {
# if (doc['model'] == 'Post' && doc.date) {
# emit(doc.date, null);
# }
# }
@ -77,7 +77,7 @@ module CouchRest
ducktype = opts.delete(:ducktype)
unless ducktype || opts[:map]
opts[:guards] ||= []
opts[:guards].push "(doc['couchrest-type'] == '#{self.to_s}')"
opts[:guards].push "(doc['#{model_type_key}'] == '#{self.to_s}')"
end
keys.push opts
design_doc.view_by(*keys)

View file

@ -4,7 +4,7 @@ require "active_model/railtie"
module CouchrestModel
# = Active Record Railtie
class Railtie < Rails::Railtie
config.generators.orm :couchrest
config.generators.orm :couchrest_model
config.generators.test_framework :test_unit, :fixture => false
end

View file

@ -35,6 +35,7 @@ require 'couchrest/model/errors'
require "couchrest/model/persistence"
require "couchrest/model/typecast"
require "couchrest/model/property"
require "couchrest/model/property_protection"
require "couchrest/model/casted_array"
require "couchrest/model/properties"
require "couchrest/model/validations"
@ -45,10 +46,8 @@ require "couchrest/model/design_doc"
require "couchrest/model/extended_attachments"
require "couchrest/model/class_proxy"
require "couchrest/model/collection"
require "couchrest/model/attribute_protection"
require "couchrest/model/attributes"
require "couchrest/model/associations"
require "couchrest/model/dirty"
require "couchrest/model/configuration"
# Monkey patches applied to couchrest
require "couchrest/model/support/couchrest"

View file

@ -36,7 +36,7 @@ describe "Model Base" do
it "should not failed on a nil value in argument" do
@obj = Basic.new(nil)
@obj.should == { 'couchrest-type' => 'Basic' }
@obj.should_not be_nil
end
end

View file

@ -0,0 +1,78 @@
# encoding: utf-8
require File.expand_path('../../spec_helper', __FILE__)
require File.join(FIXTURE_PATH, 'more', 'cat')
describe CouchRest::Model::Base do
before do
@class = Class.new(CouchRest::Model::Base)
end
describe '.configure' do
it "should set a configuration parameter" do
@class.add_config :foo_bar
@class.configure do |config|
config.foo_bar = 'monkey'
end
@class.foo_bar.should == 'monkey'
end
end
describe '.add_config' do
it "should add a class level accessor" do
@class.add_config :foo_bar
@class.foo_bar = 'foo'
@class.foo_bar.should == 'foo'
end
['foo', :foo, 45, ['foo', :bar]].each do |val|
it "should be inheritable for a #{val.class}" do
@class.add_config :foo_bar
@child_class = Class.new(@class)
@class.foo_bar = val
@class.foo_bar.should == val
@child_class.foo_bar.should == val
@child_class.foo_bar = "bar"
@child_class.foo_bar.should == "bar"
@class.foo_bar.should == val
end
end
it "should add an instance level accessor" do
@class.add_config :foo_bar
@class.foo_bar = 'foo'
@class.new.foo_bar.should == 'foo'
end
it "should add a convenient in-class setter" do
@class.add_config :foo_bar
@class.foo_bar "monkey"
@class.foo_bar.should == "monkey"
end
end
describe "General examples" do
before(:all) do
@default_model_key = 'model'
end
it "should be possible to override on class using configure method" do
default_model_key = Cat.model_type_key
Cat.instance_eval do
model_type_key 'cat-type'
end
CouchRest::Model::Base.model_type_key.should eql(default_model_key)
Cat.model_type_key.should eql('cat-type')
cat = Cat.new
cat.model_type_key.should eql('cat-type')
end
end
end

View file

@ -1,126 +0,0 @@
require File.expand_path("../../spec_helper", __FILE__)
class DirtyModel < CouchRest::Model::Base
use_database TEST_SERVER.default_database
property :name
property :color
validates_presence_of :name
end
describe 'Dirty Tracking', '#changed?' do
before(:each) do
@dm = DirtyModel.new
@dm.name = 'will'
end
it 'brand new models should not be changed by default' do
DirtyModel.new.should_not be_changed
end
it 'save should reset changed?' do
@dm.should be_changed
@dm.save
@dm.should_not be_changed
end
it 'save! should reset changed?' do
@dm.should be_changed
@dm.save!
@dm.should_not be_changed
end
it 'a failed save should preserve changed?' do
@dm.name = ''
@dm.should be_changed
@dm.save.should be_false
@dm.should be_changed
end
it 'should be true if there have been changes' do
@dm.name = 'not will'
@dm.should be_changed
end
end
describe 'Dirty Tracking', '#changed' do
it 'should be an array of the changed attributes' do
dm = DirtyModel.new
dm.changed.should == []
dm.name = 'will'
dm.changed.should == ['name']
dm.color = 'red'
dm.changed.should =~ ['name', 'color']
end
end
describe 'Dirty Tracking', '#changes' do
it 'should be a Map of changed attrs => [original value, new value]' do
dm = DirtyModel.new(:name => 'will', :color => 'red')
dm.save!
dm.should_not be_changed
dm.name = 'william'
dm.color = 'blue'
dm.changes.should == { 'name' => ['will', 'william'], 'color' => ['red', 'blue'] }
end
end
describe 'Dirty Tracking', '#previous_changes' do
it 'should store the previous changes after a save' do
dm = DirtyModel.new(:name => 'will', :color => 'red')
dm.save!
dm.should_not be_changed
dm.name = 'william'
dm.save!
dm.previous_changes.should == { 'name' => ['will', 'william'] }
end
end
describe 'Dirty Tracking', 'attribute methods' do
before(:each) do
@dm = DirtyModel.new(:name => 'will', :color => 'red')
@dm.save!
end
describe '#<attr>_changed?' do
it 'it should know if a specific property was changed' do
@dm.name = 'william'
@dm.should be_name_changed
@dm.should_not be_color_changed
end
end
describe 'Dirty Tracking', '#<attr>_change' do
it 'should be an array of [original value, current value]' do
@dm.name = 'william'
@dm.name_change.should == ['will', 'william']
end
end
describe 'Dirty Tracking', '#<attr>_was' do
it 'should return what the attribute was' do
@dm.name = 'william'
@dm.name_was.should == 'will'
end
end
describe 'Dirty Tracking', '#reset_<attr>!' do
it 'should reset the attribute to what it was' do
@dm.name = 'william'
@dm.reset_name!
@dm.name.should == 'will'
end
end
describe 'Dirty Tracking', '#<attr>_will_change!' do
it 'should manually mark the attribute as changed' do
@dm.should_not be_name_changed
@dm.name_will_change!
@dm.should be_name_changed
end
end
end

View file

@ -25,7 +25,7 @@ describe "Model Persistence" do
end
it "should instantialize document of different type" do
doc = Article.create_from_database({'_id' => 'testitem2', '_rev' => 123, 'couchrest-type' => 'WithTemplateAndUniqueID', 'name' => 'my test'})
doc = Article.create_from_database({'_id' => 'testitem2', '_rev' => 123, Article.model_type_key => 'WithTemplateAndUniqueID', 'name' => 'my test'})
doc.class.should eql(WithTemplateAndUniqueID)
end
@ -114,7 +114,7 @@ describe "Model Persistence" do
end
it "should set the type" do
@sobj['couchrest-type'].should == 'Basic'
@sobj[@sobj.model_type_key].should == 'Basic'
end
it "should accept true or false on save for validation" do

View file

@ -34,6 +34,12 @@ describe "Model Attributes" do
user.name.should == "will"
user.phone.should == "555-5555"
end
it "should provide a list of all properties as accessible" do
user = NoProtection.new(:name => "will", :phone => "555-5555")
user.accessible_properties.length.should eql(2)
user.protected_properties.should be_empty
end
end
describe "Model Base", "accessible flag" do
@ -65,6 +71,12 @@ describe "Model Attributes" do
user.name.should == "will"
user.admin.should == false
end
it "should provide correct accessible and protected property lists" do
user = WithAccessible.new(:name => 'will', :admin => true)
user.accessible_properties.map{|p| p.to_s}.should eql(['name'])
user.protected_properties.map{|p| p.to_s}.should eql(['admin'])
end
end
describe "Model Base", "protected flag" do
@ -96,6 +108,21 @@ describe "Model Attributes" do
user.name.should == "will"
user.admin.should == false
end
it "should not modify the provided attribute hash" do
user = WithProtected.new
attrs = {:name => "will", :admin => true}
user.attributes = attrs
attrs[:admin].should be_true
attrs[:name].should eql('will')
end
it "should provide correct accessible and protected property lists" do
user = WithProtected.new(:name => 'will', :admin => true)
user.accessible_properties.map{|p| p.to_s}.should eql(['name'])
user.protected_properties.map{|p| p.to_s}.should eql(['admin'])
end
end
describe "Model Base", "mixing protected and accessible flags" do
@ -115,6 +142,7 @@ describe "Model Attributes" do
user.admin.should == false
user.phone.should == 'unset phone number'
end
end
describe "from database" do
@ -150,7 +178,7 @@ describe "Model Attributes" do
it "Base#all should not strip protected attributes" do
# all creates a CollectionProxy
docs = WithProtected.all(:key => @user.id)
docs.size.should == 1
docs.length.should == 1
reloaded = docs.first
verify_attrs reloaded
end

View file

@ -9,20 +9,6 @@ require File.join(FIXTURE_PATH, 'more', 'event')
require File.join(FIXTURE_PATH, 'more', 'user')
require File.join(FIXTURE_PATH, 'more', 'course')
describe 'Attributes' do
class AttrDoc < CouchRest::Model::Base
property :one
property :two
end
it '.attributes should have an array of attribute names' do
AttrDoc.attributes.should =~ ['two', 'one']
end
it '#attributes should have an array of attribute names' do
AttrDoc.new.attributes.should =~ ['two', 'one']
end
end
describe "Model properties" do
@ -107,6 +93,7 @@ describe "Model properties" do
expect { @card.write_attribute(:this_property_should_not_exist, 823) }.to raise_error(ArgumentError)
end
it "should let you use write_attribute on readonly properties" do
lambda {
@card.read_only_value = "foo"
@ -128,6 +115,34 @@ describe "Model properties" do
end
end
describe "mass updating attributes without property" do
describe "when mass_assign_any_attribute false" do
it "should not allow them to be set" do
@card.attributes = {:test => 'fooobar'}
@card['test'].should be_nil
end
end
describe "when mass_assign_any_attribute true" do
before(:each) do
# dup Card class so that no other tests are effected
card_class = Card.dup
card_class.class_eval do
mass_assign_any_attribute true
end
@card = card_class.new(:first_name => 'Sam')
end
it 'should allow them to be updated' do
@card.attributes = {:test => 'fooobar'}
@card['test'].should eql('fooobar')
end
end
end
describe "mass assignment protection" do
@ -310,12 +325,28 @@ describe "Model properties" do
@course['estimate'].should eql(-24.35)
end
it 'return float of a number with commas instead of points for decimals' do
@course.estimate = '23,35'
@course['estimate'].should eql(23.35)
end
it "should handle numbers with commas and points" do
@course.estimate = '1,234.00'
@course.estimate.should eql(1234.00)
end
it "should handle a mis-match of commas and points and maintain the last one" do
@course.estimate = "1,232.434.123,323"
@course.estimate.should eql(1232434123.323)
end
[ Object.new, true, '00.0', '0.', '-.0', 'string' ].each do |value|
it "does not typecast non-numeric value #{value.inspect}" do
@course.estimate = value
@course['estimate'].should equal(value)
end
end
end
describe 'when type primitive is a Integer' do

View file

@ -92,8 +92,8 @@ describe "Subclassing a Model" do
OnlineCourse.design_doc['views'].keys.should_not include('by_title')
end
it "should have an all view with a guard clause for couchrest-type == subclass name in the map function" do
OnlineCourse.design_doc['views']['all']['map'].should =~ /if \(doc\['couchrest-type'\] == 'OnlineCourse'\)/
it "should have an all view with a guard clause for model == subclass name in the map function" do
OnlineCourse.design_doc['views']['all']['map'].should =~ /if \(doc\['#{OnlineCourse.model_type_key}'\] == 'OnlineCourse'\)/
end
end

View file

@ -9,7 +9,7 @@ class Article < CouchRest::Model::Base
view_by :tags,
:map =>
"function(doc) {
if (doc['couchrest-type'] == 'Article' && doc.tags) {
if (doc['#{model_type_key}'] == 'Article' && doc.tags) {
doc.tags.forEach(function(tag){
emit(tag, 1);
});

View file

@ -10,7 +10,7 @@ unless defined?(FIXTURE_PATH)
COUCHHOST = "http://127.0.0.1:5984"
TESTDB = 'couchrest-model-test'
TEST_SERVER = CouchRest.new
TEST_SERVER = CouchRest.new COUCHHOST
TEST_SERVER.default_database = TESTDB
DB = TEST_SERVER.database(TESTDB)
end