adding rdoc to method properties_with_values
This commit is contained in:
commit
92a10dbfc9
30 changed files with 764 additions and 506 deletions
162
README.md
162
README.md
|
@ -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
|
||||
|
||||
|
|
2
Rakefile
2
Rakefile
|
@ -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")
|
||||
|
|
|
@ -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"])
|
||||
|
|
14
history.txt
14
history.txt
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
@ -4,24 +4,23 @@ module CouchRest
|
|||
|
||||
extend ActiveModel::Naming
|
||||
|
||||
include CouchRest::Model::Configuration
|
||||
include CouchRest::Model::Persistence
|
||||
include CouchRest::Model::Callbacks
|
||||
include CouchRest::Model::DocumentQueries
|
||||
include CouchRest::Model::DocumentQueries
|
||||
include CouchRest::Model::Views
|
||||
include CouchRest::Model::DesignDoc
|
||||
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 ||= []
|
||||
end
|
||||
|
||||
|
||||
def self.inherited(subklass)
|
||||
super
|
||||
subklass.send(:include, CouchRest::Model::Properties)
|
||||
|
@ -35,7 +34,7 @@ module CouchRest
|
|||
EOS
|
||||
subclasses << subklass
|
||||
end
|
||||
|
||||
|
||||
# Accessors
|
||||
attr_accessor :casted_by
|
||||
|
||||
|
@ -44,19 +43,19 @@ module CouchRest
|
|||
# using the provided document hash.
|
||||
#
|
||||
# Options supported:
|
||||
#
|
||||
#
|
||||
# * :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
|
||||
|
||||
|
||||
|
||||
|
||||
# Temp solution to make the view_by methods available
|
||||
def self.method_missing(m, *args, &block)
|
||||
if has_view?(m)
|
||||
|
@ -70,9 +69,9 @@ module CouchRest
|
|||
end
|
||||
super
|
||||
end
|
||||
|
||||
|
||||
### instance methods
|
||||
|
||||
|
||||
# Gets a reference to the actual document in the DB
|
||||
# Calls up to the next document if there is one,
|
||||
# Otherwise we're at the top and we return self
|
||||
|
@ -80,14 +79,14 @@ module CouchRest
|
|||
return self if base_doc?
|
||||
@casted_by.base_doc
|
||||
end
|
||||
|
||||
|
||||
# Checks if we're the top document
|
||||
def base_doc?
|
||||
!@casted_by
|
||||
end
|
||||
|
||||
|
||||
## Compatibility with ActiveSupport and older frameworks
|
||||
|
||||
|
||||
# Hack so that CouchRest::Document, which descends from Hash,
|
||||
# doesn't appear to Rails routing as a Hash of options
|
||||
def is_a?(klass)
|
||||
|
@ -99,14 +98,14 @@ module CouchRest
|
|||
def persisted?
|
||||
!new?
|
||||
end
|
||||
|
||||
|
||||
def to_key
|
||||
new? ? nil : [id]
|
||||
new? ? nil : [id]
|
||||
end
|
||||
|
||||
alias :to_param :id
|
||||
alias :new_record? :new?
|
||||
alias :new_document? :new?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,39 +1,39 @@
|
|||
module CouchRest::Model
|
||||
module CastedModel
|
||||
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
||||
def initialize(keys = {})
|
||||
raise StandardError unless self.is_a? Hash
|
||||
prepare_all_attributes(keys)
|
||||
super()
|
||||
end
|
||||
|
||||
|
||||
def []= key, value
|
||||
super(key.to_s, value)
|
||||
end
|
||||
|
||||
|
||||
def [] key
|
||||
super(key.to_s)
|
||||
end
|
||||
|
||||
|
||||
# Gets a reference to the top level extended
|
||||
# document that a model is saved inside of
|
||||
def base_doc
|
||||
return nil unless @casted_by
|
||||
@casted_by.base_doc
|
||||
end
|
||||
|
||||
|
||||
# False if the casted model has already
|
||||
# been saved in the containing document
|
||||
def new?
|
||||
|
@ -53,12 +53,12 @@ module CouchRest::Model
|
|||
end
|
||||
alias :to_key :id
|
||||
alias :to_param :id
|
||||
|
||||
|
||||
# Sets the attributes from a hash
|
||||
def update_attributes_without_saving(hash)
|
||||
hash.each do |k, v|
|
||||
raise NoMethodError, "#{k}= method not available, use property :#{k}" unless self.respond_to?("#{k}=")
|
||||
end
|
||||
end
|
||||
hash.each do |k, v|
|
||||
self.send("#{k}=",v)
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
51
lib/couchrest/model/configuration.rb
Normal file
51
lib/couchrest/model/configuration.rb
Normal 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
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}"
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
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?(:[]=))
|
||||
included do
|
||||
extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
|
||||
self.properties ||= []
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
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
|
|
@ -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
190
lib/couchrest/model/view.rb
Normal 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
|
|
@ -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)
|
||||
|
|
|
@ -4,9 +4,9 @@ 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
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -8,23 +8,23 @@ require File.join(FIXTURE_PATH, 'more', 'card')
|
|||
require File.join(FIXTURE_PATH, 'base')
|
||||
|
||||
describe "Model Base" do
|
||||
|
||||
|
||||
before(:each) do
|
||||
@obj = WithDefaultValues.new
|
||||
end
|
||||
|
||||
|
||||
describe "instance database connection" do
|
||||
it "should use the default database" do
|
||||
@obj.database.name.should == 'couchrest-model-test'
|
||||
end
|
||||
|
||||
|
||||
it "should override the default db" do
|
||||
@obj.database = TEST_SERVER.database!('couchrest-extendedmodel-test')
|
||||
@obj.database.name.should == 'couchrest-extendedmodel-test'
|
||||
@obj.database.delete!
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe "a new model" do
|
||||
it "should be a new document" do
|
||||
@obj = Basic.new
|
||||
|
@ -36,13 +36,13 @@ 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
|
||||
|
||||
|
||||
describe "ActiveModel compatability Basic" do
|
||||
|
||||
before(:each) do
|
||||
before(:each) do
|
||||
@obj = Basic.new(nil)
|
||||
end
|
||||
|
||||
|
@ -86,7 +86,7 @@ describe "Model Base" do
|
|||
context "when the document is not new" do
|
||||
it "returns id" do
|
||||
@obj.save
|
||||
@obj.persisted?.should == true
|
||||
@obj.persisted?.should == true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -100,7 +100,7 @@ describe "Model Base" do
|
|||
|
||||
|
||||
end
|
||||
|
||||
|
||||
describe "update attributes without saving" do
|
||||
before(:each) do
|
||||
a = Article.get "big-bad-danger" rescue nil
|
||||
|
@ -134,22 +134,22 @@ describe "Model Base" do
|
|||
@art.attributes = {'date' => Time.now, :title => "something else"}
|
||||
@art['title'].should == "something else"
|
||||
end
|
||||
|
||||
|
||||
it "should not flip out if an attribute= method is missing and ignore it" do
|
||||
lambda {
|
||||
@art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger")
|
||||
}.should_not raise_error
|
||||
@art.slug.should == "big-bad-danger"
|
||||
end
|
||||
|
||||
|
||||
#it "should not change other attributes if there is an error" do
|
||||
# lambda {
|
||||
# @art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger")
|
||||
# @art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger")
|
||||
# }.should raise_error
|
||||
# @art['title'].should == "big bad danger"
|
||||
#end
|
||||
end
|
||||
|
||||
|
||||
describe "update attributes" do
|
||||
before(:each) do
|
||||
a = Article.get "big-bad-danger" rescue nil
|
||||
|
@ -164,7 +164,7 @@ describe "Model Base" do
|
|||
loaded['title'].should == "super danger"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe "with default" do
|
||||
it "should have the default value set at initalization" do
|
||||
@obj.preset.should == {:right => 10, :top_align => false}
|
||||
|
@ -173,23 +173,23 @@ describe "Model Base" do
|
|||
it "should have the default false value explicitly assigned" do
|
||||
@obj.default_false.should == false
|
||||
end
|
||||
|
||||
|
||||
it "should automatically call a proc default at initialization" do
|
||||
@obj.set_by_proc.should be_an_instance_of(Time)
|
||||
@obj.set_by_proc.should == @obj.set_by_proc
|
||||
@obj.set_by_proc.should < Time.now
|
||||
end
|
||||
|
||||
|
||||
it "should let you overwrite the default values" do
|
||||
obj = WithDefaultValues.new(:preset => 'test')
|
||||
obj.preset = 'test'
|
||||
end
|
||||
|
||||
|
||||
it "should work with a default empty array" do
|
||||
obj = WithDefaultValues.new(:tags => ['spec'])
|
||||
obj.tags.should == ['spec']
|
||||
end
|
||||
|
||||
|
||||
it "should set default value of read-only property" do
|
||||
obj = WithDefaultValues.new
|
||||
obj.read_only_with_default.should == 'generic'
|
||||
|
@ -207,7 +207,7 @@ describe "Model Base" do
|
|||
obj.tags.should == ['spec']
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe "a doc with template values (CR::Model spec)" do
|
||||
before(:all) do
|
||||
WithTemplateAndUniqueID.all.map{|o| o.destroy}
|
||||
|
@ -228,8 +228,8 @@ describe "Model Base" do
|
|||
tmpl2_reloaded.preset.should == 'not_value'
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
describe "finding all instances of a model" do
|
||||
before(:all) do
|
||||
WithTemplateAndUniqueID.req_design_doc_refresh
|
||||
|
@ -246,32 +246,32 @@ describe "Model Base" do
|
|||
d['views']['all']['map'].should include('WithTemplateAndUniqueID')
|
||||
end
|
||||
it "should find all" do
|
||||
rs = WithTemplateAndUniqueID.all
|
||||
rs = WithTemplateAndUniqueID.all
|
||||
rs.length.should == 4
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe "counting all instances of a model" do
|
||||
before(:each) do
|
||||
@db = reset_test_db!
|
||||
WithTemplateAndUniqueID.req_design_doc_refresh
|
||||
end
|
||||
|
||||
|
||||
it ".count should return 0 if there are no docuemtns" do
|
||||
WithTemplateAndUniqueID.count.should == 0
|
||||
end
|
||||
|
||||
|
||||
it ".count should return the number of documents" do
|
||||
WithTemplateAndUniqueID.new('important-field' => '1').save
|
||||
WithTemplateAndUniqueID.new('important-field' => '2').save
|
||||
WithTemplateAndUniqueID.new('important-field' => '3').save
|
||||
|
||||
|
||||
WithTemplateAndUniqueID.count.should == 3
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe "finding the first instance of a model" do
|
||||
before(:each) do
|
||||
before(:each) do
|
||||
@db = reset_test_db!
|
||||
# WithTemplateAndUniqueID.req_design_doc_refresh # Removed by Sam Lown, design doc should be loaded automatically
|
||||
WithTemplateAndUniqueID.new('important-field' => '1').save
|
||||
|
@ -309,7 +309,7 @@ describe "Model Base" do
|
|||
WithTemplateAndUniqueID.design_doc['_rev'].should eql(rev)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe "getting a model with a subobject field" do
|
||||
before(:all) do
|
||||
course_doc = {
|
||||
|
@ -332,7 +332,7 @@ describe "Model Base" do
|
|||
@course['ends_at'].should == Time.parse("2008/12/19 13:00:00 +0800")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe "timestamping" do
|
||||
before(:each) do
|
||||
oldart = Article.get "saving-this" rescue nil
|
||||
|
@ -340,7 +340,7 @@ describe "Model Base" do
|
|||
@art = Article.new(:title => "Saving this")
|
||||
@art.save
|
||||
end
|
||||
|
||||
|
||||
it "should define the updated_at and created_at getters and set the values" do
|
||||
@obj.save
|
||||
obj = WithDefaultValues.get(@obj.id)
|
||||
|
@ -349,15 +349,15 @@ describe "Model Base" do
|
|||
obj.updated_at.should be_an_instance_of(Time)
|
||||
obj.created_at.to_s.should == @obj.updated_at.to_s
|
||||
end
|
||||
|
||||
|
||||
it "should not change created_at on update" do
|
||||
2.times do
|
||||
2.times do
|
||||
lambda do
|
||||
@art.save
|
||||
end.should_not change(@art, :created_at)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
it "should set the time on create" do
|
||||
(Time.now - @art.created_at).should < 2
|
||||
foundart = Article.get @art.id
|
||||
|
@ -368,7 +368,7 @@ describe "Model Base" do
|
|||
@art.created_at.should < @art.updated_at
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe "getter and setter methods" do
|
||||
it "should try to call the arg= method before setting :arg in the hash" do
|
||||
@doc = WithGetterAndSetterMethods.new(:arg => "foo")
|
||||
|
@ -384,41 +384,41 @@ describe "Model Base" do
|
|||
@doc['some_value'].should eql('value')
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe "recursive validation on a model" do
|
||||
before :each do
|
||||
reset_test_db!
|
||||
@cat = Cat.new(:name => 'Sockington')
|
||||
end
|
||||
|
||||
|
||||
it "should not save if a nested casted model is invalid" do
|
||||
@cat.favorite_toy = CatToy.new
|
||||
@cat.should_not be_valid
|
||||
@cat.save.should be_false
|
||||
lambda{@cat.save!}.should raise_error
|
||||
end
|
||||
|
||||
|
||||
it "should save when nested casted model is valid" do
|
||||
@cat.favorite_toy = CatToy.new(:name => 'Squeaky')
|
||||
@cat.should be_valid
|
||||
@cat.save.should be_true
|
||||
lambda{@cat.save!}.should_not raise_error
|
||||
end
|
||||
|
||||
|
||||
it "should not save when nested collection contains an invalid casted model" do
|
||||
@cat.toys = [CatToy.new(:name => 'Feather'), CatToy.new]
|
||||
@cat.should_not be_valid
|
||||
@cat.save.should be_false
|
||||
lambda{@cat.save!}.should raise_error
|
||||
end
|
||||
|
||||
|
||||
it "should save when nested collection contains valid casted models" do
|
||||
@cat.toys = [CatToy.new(:name => 'feather'), CatToy.new(:name => 'ball-o-twine')]
|
||||
@cat.should be_valid
|
||||
@cat.save.should be_true
|
||||
lambda{@cat.save!}.should_not raise_error
|
||||
end
|
||||
|
||||
|
||||
it "should not fail if the nested casted model doesn't have validation" do
|
||||
Cat.property :trainer, Person
|
||||
Cat.validates_presence_of :name
|
||||
|
|
78
spec/couchrest/configuration_spec.rb
Normal file
78
spec/couchrest/configuration_spec.rb
Normal 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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
2
spec/fixtures/more/article.rb
vendored
2
spec/fixtures/more/article.rb
vendored
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue