couchobject 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +10 -0
- data/Manifest.txt +30 -6
- data/README.txt +580 -42
- data/TODO +2 -2
- data/config/hoe.rb +1 -1
- data/lib/couch_object.rb +7 -2
- data/lib/couch_object/database.rb +19 -34
- data/lib/couch_object/document.rb +13 -6
- data/lib/couch_object/error_classes.rb +110 -0
- data/lib/couch_object/persistable.rb +954 -36
- data/lib/couch_object/persistable/has_many_relations_array.rb +91 -0
- data/lib/couch_object/persistable/meta_classes.rb +568 -0
- data/lib/couch_object/persistable/overloaded_methods.rb +209 -0
- data/lib/couch_object/server.rb +1 -1
- data/lib/couch_object/utils.rb +44 -0
- data/lib/couch_object/version.rb +1 -1
- data/lib/couch_object/view.rb +129 -6
- data/script/console +0 -0
- data/script/destroy +0 -0
- data/script/generate +0 -0
- data/script/txt2html +0 -0
- data/spec/database_spec.rb +23 -31
- data/spec/database_spec.rb.orig +173 -0
- data/spec/document_spec.rb +21 -3
- data/spec/integration/database_integration_spec.rb +46 -15
- data/spec/integration/integration_helper.rb +3 -3
- data/spec/persistable/callback.rb +44 -0
- data/spec/persistable/callback_spec.rb +44 -0
- data/spec/persistable/cloning.rb +77 -0
- data/spec/persistable/cloning_spec.rb +77 -0
- data/spec/persistable/comparing_objects.rb +350 -0
- data/spec/persistable/comparing_objects_spec.rb +350 -0
- data/spec/persistable/deleting.rb +113 -0
- data/spec/persistable/deleting_spec.rb +113 -0
- data/spec/persistable/error_messages.rb +32 -0
- data/spec/persistable/error_messages_spec.rb +32 -0
- data/spec/persistable/loading.rb +339 -0
- data/spec/persistable/loading_spec.rb +339 -0
- data/spec/persistable/new_methods.rb +70 -0
- data/spec/persistable/new_methods_spec.rb +70 -0
- data/spec/persistable/persistable_helper.rb +194 -0
- data/spec/persistable/relations.rb +470 -0
- data/spec/persistable/relations_spec.rb +470 -0
- data/spec/persistable/saving.rb +137 -0
- data/spec/persistable/saving_spec.rb +137 -0
- data/spec/persistable/setting_storage_location.rb +65 -0
- data/spec/persistable/setting_storage_location_spec.rb +65 -0
- data/spec/persistable/timestamps.rb +76 -0
- data/spec/persistable/timestamps_spec.rb +76 -0
- data/spec/persistable/unsaved_changes.rb +211 -0
- data/spec/persistable/unsaved_changes_spec.rb +211 -0
- data/spec/server_spec.rb +5 -5
- data/spec/utils_spec.rb +60 -0
- data/spec/view_spec.rb +40 -7
- data/website/index.html +22 -7
- data/website/index.txt +13 -5
- metadata +93 -61
- data/bin/couch_ruby_view_requestor +0 -81
- data/lib/couch_object/model.rb +0 -5
- data/lib/couch_object/proc_condition.rb +0 -14
- data/spec/model_spec.rb +0 -5
- data/spec/persistable_spec.rb +0 -91
- data/spec/proc_condition_spec.rb +0 -26
data/TODO
CHANGED
data/config/hoe.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'couch_object/version'
|
2
2
|
|
3
|
-
AUTHOR = 'Johan Sørensen' # can also be an array of Authors
|
3
|
+
AUTHOR = ['Johan Sørensen','Sebastian Probst Eide'] # can also be an array of Authors
|
4
4
|
EMAIL = "johan@johansorensen.com"
|
5
5
|
DESCRIPTION = "CouchObject is a library that maps ruby objects to CouchDb documents"
|
6
6
|
GEM_NAME = 'couchobject' # what ppl will type to install your gem
|
data/lib/couch_object.rb
CHANGED
@@ -11,12 +11,16 @@ rescue LoadError
|
|
11
11
|
end
|
12
12
|
|
13
13
|
require 'json/add/core'
|
14
|
-
require
|
14
|
+
require 'thread'
|
15
|
+
|
16
|
+
# require "htmlentities" # if you want the UTILS::decode_strings to
|
17
|
+
# replace characters like ø with ø
|
18
|
+
# then this has to be included...
|
19
|
+
require "kconv" # To convert all input to UTF-8 before it is made to_json
|
15
20
|
|
16
21
|
$:.unshift File.dirname(__FILE__)
|
17
22
|
|
18
23
|
require "couch_object/utils"
|
19
|
-
require "couch_object/proc_condition"
|
20
24
|
require "couch_object/document"
|
21
25
|
require "couch_object/response"
|
22
26
|
require "couch_object/server"
|
@@ -24,6 +28,7 @@ require "couch_object/database"
|
|
24
28
|
require "couch_object/view"
|
25
29
|
require "couch_object/persistable"
|
26
30
|
require "couch_object/model"
|
31
|
+
require "couch_object/error_classes"
|
27
32
|
|
28
33
|
module CouchObject
|
29
34
|
|
@@ -2,7 +2,7 @@ module CouchObject
|
|
2
2
|
# A CouchDb database object
|
3
3
|
class Database
|
4
4
|
# Create a new database at +uri+ with the name if +dbname+
|
5
|
-
def self.create
|
5
|
+
def self.create(uri, dbname)
|
6
6
|
server = Server.new(uri)
|
7
7
|
response = Response.new(server.put("/#{dbname}", "")).parse
|
8
8
|
response.parsed_body
|
@@ -25,7 +25,7 @@ module CouchObject
|
|
25
25
|
end
|
26
26
|
|
27
27
|
# Open a connection to the database at +uri+, where +uri+ is a full uri
|
28
|
-
# like: http://localhost:
|
28
|
+
# like: http://localhost:5984/foo
|
29
29
|
def self.open(uri)
|
30
30
|
uri = URI.parse(uri)
|
31
31
|
server_uri = "#{uri.scheme}://#{uri.host}:#{uri.port}"
|
@@ -39,7 +39,7 @@ module CouchObject
|
|
39
39
|
end
|
40
40
|
attr_accessor :server
|
41
41
|
|
42
|
-
# The full url of this database, eg http://localhost:
|
42
|
+
# The full url of this database, eg http://localhost:5984/foo
|
43
43
|
def url
|
44
44
|
Utils.join_url(@uri, @dbname).to_s
|
45
45
|
end
|
@@ -50,35 +50,37 @@ module CouchObject
|
|
50
50
|
end
|
51
51
|
|
52
52
|
# Send a GET request to the +path+ which is relative to the database path
|
53
|
-
# so calling with with "bar" as the path in the "foo_db" database will
|
54
|
-
# http://host:port/foo_db/bar.
|
53
|
+
# so calling with with "bar" as the path in the "foo_db" database will
|
54
|
+
# call http://host:port/foo_db/bar.
|
55
55
|
# Returns a Response object
|
56
56
|
def get(path)
|
57
57
|
Response.new(@server.get("/#{Utils.join_url(@dbname, path)}")).parse
|
58
58
|
end
|
59
59
|
|
60
60
|
# Send a POST request to the +path+ which is relative to the database path
|
61
|
-
# so calling with with "bar" as the path in the "foo_db" database will
|
62
|
-
# http://host:port/foo_db/bar. The post body is the +payload+
|
61
|
+
# so calling with with "bar" as the path in the "foo_db" database will
|
62
|
+
# call http://host:port/foo_db/bar. The post body is the +payload+
|
63
63
|
# Returns a Response object
|
64
|
-
def post(path, payload)
|
65
|
-
Response.new(@server.post("/#{Utils.join_url(@dbname, path)}", payload)).parse
|
64
|
+
def post(path, payload, content_type="application/json")
|
65
|
+
Response.new(@server.post("/#{Utils.join_url(@dbname, path)}", payload, content_type)).parse
|
66
66
|
end
|
67
67
|
|
68
68
|
# Send a PUT request to the +path+ which is relative to the database path
|
69
|
-
# so calling with with "bar" as the path in the "foo_db" database will
|
70
|
-
# http://host:port/foo_db/bar. The put body is the +payload+
|
69
|
+
# so calling with with "bar" as the path in the "foo_db" database will
|
70
|
+
# call http://host:port/foo_db/bar. The put body is the +payload+
|
71
71
|
# Returns a Response object
|
72
72
|
def put(path, payload="")
|
73
|
-
Response.new(@server.
|
73
|
+
Response.new(@server.
|
74
|
+
put("/#{Utils.join_url(@dbname, path)}", payload)).parse
|
74
75
|
end
|
75
76
|
|
76
|
-
# Send a DELETE request to the +path+ which is relative to the
|
77
|
-
# so calling with with "bar" as the path in the "foo_db"
|
78
|
-
# http://host:port/foo_db/bar.
|
77
|
+
# Send a DELETE request to the +path+ which is relative to the
|
78
|
+
# database path so calling with with "bar" as the path in the "foo_db"
|
79
|
+
# database will call http://host:port/foo_db/bar.
|
79
80
|
# Returns a Response object
|
80
|
-
def delete(path)
|
81
|
-
Response.new(@server.
|
81
|
+
def delete(path, revision)
|
82
|
+
Response.new(@server.
|
83
|
+
delete("/#{Utils.join_url(@dbname, path)}?rev=#{revision}")).parse
|
82
84
|
end
|
83
85
|
|
84
86
|
# Get a document by id
|
@@ -105,23 +107,6 @@ module CouchObject
|
|
105
107
|
document.save(self)
|
106
108
|
end
|
107
109
|
|
108
|
-
# Queries the database with the block (using a temp. view)
|
109
|
-
# Requires a block argument that's the doc thats evaluted in
|
110
|
-
# CouchDb
|
111
|
-
#
|
112
|
-
# >> pp db.filter do |doc|
|
113
|
-
# if doc["foo"] == "baz"
|
114
|
-
# return doc["foo"]
|
115
|
-
# end
|
116
|
-
# end
|
117
|
-
# [{"_rev"=>928806717,
|
118
|
-
# "_id"=>"28D568C5992CBD2B4711F57225A19517",
|
119
|
-
# "value"=>"baz"}]
|
120
|
-
def filter(&block)
|
121
|
-
resp = Response.new(post("_temp_view", ProcCondition.new(&block).to_ruby)).parse
|
122
|
-
resp.to_document.rows
|
123
|
-
end
|
124
|
-
|
125
110
|
def views(view_name)
|
126
111
|
view = View.new(self, view_name)
|
127
112
|
view.query
|
@@ -7,8 +7,8 @@ module CouchObject
|
|
7
7
|
# the document values
|
8
8
|
def initialize(attributes={})
|
9
9
|
@attributes = attributes.dup
|
10
|
-
@id = @attributes.delete("_id")
|
11
|
-
@revision = @attributes.delete("_rev")
|
10
|
+
@id = @attributes.delete("_id") || @attributes.delete("id")
|
11
|
+
@revision = @attributes.delete("_rev") || @attributes.delete("rev")
|
12
12
|
end
|
13
13
|
attr_accessor :attributes, :id, :revision
|
14
14
|
|
@@ -79,14 +79,14 @@ module CouchObject
|
|
79
79
|
def create(database)
|
80
80
|
response = database.post("", self.to_json)
|
81
81
|
# TODO error handling
|
82
|
-
|
83
|
-
@revision = response.to_document.revision
|
84
|
-
response
|
82
|
+
apply_response(response)
|
85
83
|
end
|
86
84
|
|
87
85
|
def update(database)
|
88
|
-
|
86
|
+
revision ? json = self.to_json("_rev" => revision) : json = self.to_json
|
87
|
+
response = database.put(id, json)
|
89
88
|
# TODO error handling
|
89
|
+
apply_response(response)
|
90
90
|
end
|
91
91
|
|
92
92
|
private
|
@@ -102,5 +102,12 @@ module CouchObject
|
|
102
102
|
has_key?(method_name) ? self[method_name] : super
|
103
103
|
end
|
104
104
|
end
|
105
|
+
|
106
|
+
def apply_response(response)
|
107
|
+
doc = response.to_document
|
108
|
+
@id = doc.id
|
109
|
+
@revision = doc.revision
|
110
|
+
response
|
111
|
+
end
|
105
112
|
end
|
106
113
|
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module CouchObject
|
2
|
+
module Errors
|
3
|
+
#
|
4
|
+
# Is raised when trying to load a view that doesn't exist on
|
5
|
+
# the couchDB server
|
6
|
+
#
|
7
|
+
class MissingView < StandardError
|
8
|
+
def message
|
9
|
+
"The view doesn't exist on the server."
|
10
|
+
end
|
11
|
+
alias_method :to_s, :message
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
#####
|
16
|
+
#
|
17
|
+
# ERRORS for the Persistable module
|
18
|
+
#
|
19
|
+
#####
|
20
|
+
|
21
|
+
#
|
22
|
+
# Is raised when a method that interacts with the
|
23
|
+
# document store is called and the location variable
|
24
|
+
# has not been set
|
25
|
+
#
|
26
|
+
class NoDatabaseLocationSet < StandardError
|
27
|
+
def message
|
28
|
+
"Unless the document has previously been saved you need to " + \
|
29
|
+
"supply the full URI to the CouchDB server where the document " + \
|
30
|
+
"is to be saved."
|
31
|
+
end
|
32
|
+
alias_method :to_s, :message
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Is raised when trying to compare two object of which
|
37
|
+
# none has been saved.
|
38
|
+
#
|
39
|
+
class CantCompareSize < StandardError
|
40
|
+
def message
|
41
|
+
"Can't compare the size of two objects unless at least one of " + \
|
42
|
+
"them has been saved and both have timestamps."
|
43
|
+
end
|
44
|
+
alias_method :to_s, :message
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# Is raised if a has_many relationship is poorly defined
|
49
|
+
#
|
50
|
+
class HasManyAssociationError < StandardError
|
51
|
+
def message
|
52
|
+
"The has_many relationship hasn't been correctly defined. It should look something like: has_many :fruits. Make sure to also include a belongs_to relationship in the related classes that reference back to the has_many relationship. For example like this: belongs_to :fruit_basket, :as => :fruits"
|
53
|
+
end
|
54
|
+
alias_method :to_s, :message
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# Is raised if a belongs_to relationship is poorly defined
|
59
|
+
#
|
60
|
+
class BelongsToAssociationError < StandardError
|
61
|
+
def message
|
62
|
+
"The belongs_to relationship hasn't been properly defined. There are two required parameters: the name of the relation and the name of the corresponding has_many relation in the related class. Example: belongs_to :fruit_basket, :as => :fruits would be correct if the related class has it's relation defined as has_many :fruits"
|
63
|
+
end
|
64
|
+
alias_method :to_s, :message
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Is raised if a has_one relationship is poorly defined
|
69
|
+
#
|
70
|
+
class HasOneAssociationError < StandardError
|
71
|
+
def message
|
72
|
+
"The has_one relationship hasn't been correctly defined. It should look something like: has_one :sword. Make sure to also include a belongs_to relationship in the related classes that reference back to the has_one relationship. For example like this: belongs_to :master, :as => :sword"
|
73
|
+
end
|
74
|
+
alias_method :to_s, :message
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
# This error is raised when trying to load a document that doesn't
|
79
|
+
# exist on the server
|
80
|
+
#
|
81
|
+
class DocumentNotFound < StandardError
|
82
|
+
end
|
83
|
+
|
84
|
+
#
|
85
|
+
# This error is raised when trying to save or updating
|
86
|
+
# a document fails
|
87
|
+
#
|
88
|
+
class DatabaseSaveFailed < StandardError
|
89
|
+
end
|
90
|
+
|
91
|
+
#
|
92
|
+
# This error is raised when trying to load a view
|
93
|
+
# but CouchDB screwes it up and returns a MapProcessError
|
94
|
+
#
|
95
|
+
class MapProcessError < StandardError
|
96
|
+
def message
|
97
|
+
"CouchDB screwed it up somehow. There might be something wrong with your view as well."
|
98
|
+
end
|
99
|
+
alias_method :to_s, :message
|
100
|
+
end
|
101
|
+
|
102
|
+
#
|
103
|
+
# Is raised when one the CouchDB returns an error which isn't covered
|
104
|
+
# by one of the functions above
|
105
|
+
#
|
106
|
+
class CouchDBError < StandardError
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
@@ -1,59 +1,977 @@
|
|
1
|
+
# = Persistable
|
2
|
+
#
|
3
|
+
# == Introduction
|
4
|
+
#
|
5
|
+
# The persistable mixin let's you save and load your classes
|
6
|
+
# to and from CoachDB using the instance method +save+ and
|
7
|
+
# the class level method +get_by_id+.
|
8
|
+
# Documentation and examples can be found in the README file.
|
9
|
+
# The specs also contain lots of examples.
|
10
|
+
#
|
11
|
+
#
|
12
|
+
# = TODOs and known issues:
|
13
|
+
#
|
14
|
+
# * TODO: create a way to solve saving conflicts. Should also be possible
|
15
|
+
# to chose if only conflicts for certain variables should be handled.
|
16
|
+
#
|
17
|
+
# * TODO: If real world usage shows that it is needed then create a reload method
|
18
|
+
# that reloads the content from the db. Might be useful if some other
|
19
|
+
# process has changed the content of the document in which case the local
|
20
|
+
# copy can't be stored to the DB. Should local changes be overwritten?
|
21
|
+
# How should differences between the version on the server and the local
|
22
|
+
# version be handeled?
|
23
|
+
#
|
24
|
+
# * ISSUE:
|
25
|
+
# When creating a new document the server does not return other values
|
26
|
+
# than the new documents ID and REVISION number. The local representation
|
27
|
+
# of the created_at and updated_at attributes are therefore set right before
|
28
|
+
# the call to the server is issued. Their values are therefore not
|
29
|
+
# guaranteed to be completely in sync with the values stored on the server!
|
30
|
+
#
|
31
|
+
# * ISSUE:
|
32
|
+
# When loading multiple objects from a view using the get_from_view
|
33
|
+
# mehtod that normally would have been in a belongs_to or has_many relation,
|
34
|
+
# their relations aren't set.
|
35
|
+
#
|
36
|
+
# * ISSUE:
|
37
|
+
# Given the following scenario: B is in a has_many/has_one relationship to A
|
38
|
+
# and both have been saved to the database. B is removed from the
|
39
|
+
# relationship and A is then saved. Saving A wont save the changed version
|
40
|
+
# of B where the relationship has been ended. Therefore: the next time A
|
41
|
+
# is loaded from the database B will be loaded as a relative!
|
42
|
+
#
|
43
|
+
# Possible sollutions:
|
44
|
+
# 1) keep a list of objects that need to be saved in A so that B gets
|
45
|
+
# saved when A is saved althouh the relationship has ended.
|
46
|
+
# 2) save B as it is removed from the relationship with A.
|
47
|
+
#
|
48
|
+
# I belive 1) to be the better solution as it doesn't prematurely save B
|
49
|
+
# in cases where you don't want the changes you do to be saved to the
|
50
|
+
# database.
|
51
|
+
# MAKE SURE: that the object that is added doesn't have the same id
|
52
|
+
# and revision number ass the object that is replaced. That happens
|
53
|
+
# when a class loads its belongs_to relations! In cases like that
|
54
|
+
# the old class should NOT be tracked and saved later!
|
55
|
+
#
|
56
|
+
$:.unshift File.dirname(__FILE__)
|
57
|
+
require 'persistable/has_many_relations_array'
|
58
|
+
require 'persistable/meta_classes'
|
59
|
+
require 'persistable/overloaded_methods'
|
60
|
+
|
1
61
|
module CouchObject
|
2
|
-
module Persistable
|
3
|
-
|
4
|
-
klazz.extend(ClassMethods)
|
5
|
-
end
|
6
|
-
|
62
|
+
module Persistable
|
63
|
+
|
7
64
|
module ClassMethods
|
8
|
-
#
|
9
|
-
|
10
|
-
|
65
|
+
#
|
66
|
+
# Loads a document from the database
|
67
|
+
#
|
68
|
+
# Aliases:
|
69
|
+
# * +get+
|
70
|
+
#
|
71
|
+
# Takes:
|
72
|
+
# * +id+: the ID of the document that should be loaded
|
73
|
+
# * +db_uri+: the uri to the database. Is optional if the database has
|
74
|
+
# been defined on class level:
|
75
|
+
#
|
76
|
+
# class SomeClass
|
77
|
+
# include CouchObject::Persistable
|
78
|
+
# database 'http://localhost:5984'
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
# Returns:
|
82
|
+
# * a fully initialized class of type self
|
83
|
+
#
|
84
|
+
# Raises:
|
85
|
+
# * CouchObject::Errors::NoDatabaseLocationSet if +db_uri+
|
86
|
+
# is blank AND has not been set on class level
|
87
|
+
# * CouchObject::Errors::DocumentNotFound if the document doesn't
|
88
|
+
# exist or has been deleted
|
89
|
+
#
|
90
|
+
def get_by_id(id, db_uri = self.location)
|
91
|
+
# Raises an error if the location variable hasn't been set
|
92
|
+
raise CouchObject::Errors::NoDatabaseLocationSet unless db_uri
|
93
|
+
|
11
94
|
db = CouchObject::Database.open(db_uri)
|
12
|
-
response = db.get(id)
|
13
|
-
|
95
|
+
response = JSON.parse(db.get(id).body)
|
96
|
+
|
97
|
+
if response["error"]
|
98
|
+
case response["reason"]
|
99
|
+
when "deleted"
|
100
|
+
raise CouchObject::Errors::DocumentNotFound, "The document has been deleted"
|
101
|
+
else
|
102
|
+
raise CouchObject::Errors::DocumentNotFound, "The document could not be found"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# creates a new object and initialize all its sub objects
|
107
|
+
new_object = couch_load_object(response)
|
108
|
+
|
109
|
+
# set the storage location it was loaded from so it can be saved
|
110
|
+
# back directly without having to supply the db_uri again
|
111
|
+
new_object.instance_variable_set("@location", db_uri)
|
112
|
+
|
113
|
+
# return the new couch object
|
114
|
+
new_object
|
115
|
+
end
|
116
|
+
|
117
|
+
#
|
118
|
+
# Alias for get_by_id
|
119
|
+
#
|
120
|
+
alias get get_by_id
|
121
|
+
|
122
|
+
#
|
123
|
+
# Takes, returns and raises the same things as +get_by_id+
|
124
|
+
#
|
125
|
+
# Creates a new object that is forced into smart save mode
|
126
|
+
# although the class it is stemming from might not have smart
|
127
|
+
# saving enabled.
|
128
|
+
#
|
129
|
+
def get_with_smart_save(id, db_uri = self.location)
|
130
|
+
|
131
|
+
new_object = self.get_by_id(id, db_uri)
|
132
|
+
|
133
|
+
# Force it into smart save mode
|
134
|
+
new_object.couch_force_smart_save
|
135
|
+
|
136
|
+
# Initialize the original state.
|
137
|
+
new_object.couch_set_initial_state
|
138
|
+
|
139
|
+
new_object
|
140
|
+
end
|
141
|
+
|
142
|
+
#
|
143
|
+
# Loads all document from a given view from the database
|
144
|
+
#
|
145
|
+
# Takes:
|
146
|
+
# * +view+ (string): the name of the view to call
|
147
|
+
# * [+params+] (hash): a hash of URL query arguments supported
|
148
|
+
# by couchDB. If omitted it defaults to not use a key
|
149
|
+
# and not update the view.
|
150
|
+
# Additionally the +db_uri+ can be set as a parameter if
|
151
|
+
# it hasn't been defined at class level.
|
152
|
+
#
|
153
|
+
# Example:
|
154
|
+
#
|
155
|
+
# AppleTree.get_from_view("foo_view",
|
156
|
+
# { :db_uri => "http://localhost:5984/mydb",
|
157
|
+
# :update => false, :key => "bar"}) => Array
|
158
|
+
#
|
159
|
+
# Returns:
|
160
|
+
# * a array of initialized classes
|
161
|
+
# (if the view includes the documents full content)
|
162
|
+
#
|
163
|
+
# Raises:
|
164
|
+
# * CouchObject::Errors::NoDatabaseLocationSet if +db_uri+
|
165
|
+
# is blank AND has not been set on class level
|
166
|
+
# * CouchObject::Errors::MissingView if the view doesn't exist
|
167
|
+
#
|
168
|
+
def get_from_view( view, params = {:key => nil,
|
169
|
+
:update => true,
|
170
|
+
:db_uri => self.location})
|
171
|
+
|
172
|
+
# Raise an error if the location variable hasn't been set
|
173
|
+
db_uri = params[:db_uri] || self.location
|
174
|
+
raise CouchObject::Errors::NoDatabaseLocationSet unless db_uri
|
175
|
+
|
176
|
+
db = CouchObject::Database.open(db_uri)
|
177
|
+
|
178
|
+
params.delete(:db_uri)
|
179
|
+
|
180
|
+
#Create a querystring with the parameters passed inn
|
181
|
+
querystring = "?"
|
182
|
+
params.each_pair do |key, value|
|
183
|
+
querystring += \
|
184
|
+
"#{key}=#{Utils.encode_querystring_parameter(value)}&"
|
185
|
+
end
|
186
|
+
querystring = querystring[0...-1]
|
187
|
+
|
188
|
+
view_with_parameters = view + querystring
|
189
|
+
|
190
|
+
objects_to_return = []
|
191
|
+
|
192
|
+
response = JSON.parse(db.get(view_with_parameters).body)
|
193
|
+
|
194
|
+
raise CouchObject::Errors::MissingView, \
|
195
|
+
"The view '#{view}' doesn't exist on the server" \
|
196
|
+
if response["error"] == "not_found"
|
197
|
+
|
198
|
+
raise CouchObject::Errors::CouchDBError, \
|
199
|
+
"CouchDB returned and error and described the problem as #{response['reason']}. \n" + \
|
200
|
+
"There might be something wrong with one of your views, or it might be missing!" \
|
201
|
+
if response["error"]
|
202
|
+
|
203
|
+
response["rows"].each do |params_for_object|
|
204
|
+
objects_to_return << couch_load_object(params_for_object["value"])
|
205
|
+
end
|
206
|
+
|
207
|
+
objects_to_return
|
208
|
+
|
209
|
+
end
|
210
|
+
|
211
|
+
protected
|
212
|
+
#
|
213
|
+
# This recursive method initializes new instances of self and
|
214
|
+
# makes sure all sub classes are also initialized.
|
215
|
+
#
|
216
|
+
# Takes:
|
217
|
+
# * A hash of parameters loaded from CoachDB
|
218
|
+
#
|
219
|
+
# Returns:
|
220
|
+
# * An fully initialized object
|
221
|
+
#
|
222
|
+
# Raises:
|
223
|
+
# * Does currently not raise any error
|
224
|
+
#
|
225
|
+
def couch_load_object(parameters)
|
226
|
+
|
227
|
+
# Getting the values that shouldn't be passed on to the initializer
|
228
|
+
id = parameters["_id"]
|
229
|
+
revision = parameters["_rev"]
|
230
|
+
created_at = parameters["created_at"]
|
231
|
+
updated_at = parameters["updated_at"]
|
232
|
+
class_type = parameters["class"]
|
233
|
+
belongs_to = parameters["belongs_to"]
|
234
|
+
|
235
|
+
new_object_from_couch = nil
|
236
|
+
|
237
|
+
# Two possible routes:
|
238
|
+
# * the class has implemented a from_couch class method, in which
|
239
|
+
# case we use it, or
|
240
|
+
# * there is no from_couch method, so we just add the attributes
|
241
|
+
# as instance variables if they aren't part of a class...
|
242
|
+
if eval("#{class_type}.respond_to?(:from_couch)")
|
243
|
+
new_object_from_couch = eval("#{class_type}.send(:from_couch," \
|
244
|
+
" parameters[\"attributes\"])")
|
245
|
+
else
|
246
|
+
new_object_from_couch = eval("#{class_type}.new")
|
247
|
+
unless parameters["attributes"] == nil
|
248
|
+
parameters["attributes"].each_key do |key|
|
249
|
+
# Check if the key value pair is a class
|
250
|
+
if parameters["attributes"][key].class == Hash && \
|
251
|
+
parameters["attributes"][key]["class"] != nil
|
252
|
+
|
253
|
+
new_object_from_couch.instance_variable_set("@#{key}", \
|
254
|
+
self.couch_load_object(parameters["attributes"][key]))
|
255
|
+
else
|
256
|
+
# Add the value to the class
|
257
|
+
new_object_from_couch.
|
258
|
+
instance_variable_set("@#{key}", \
|
259
|
+
parameters["attributes"][key])
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# Sets couch_object related values
|
266
|
+
new_object_from_couch.
|
267
|
+
instance_variable_set("@revision", revision) if revision
|
268
|
+
|
269
|
+
new_object_from_couch.instance_variable_set("@id", id) if id
|
270
|
+
|
271
|
+
new_object_from_couch.instance_variable_set("@created_at", \
|
272
|
+
created_at) if eval("#{class_type}." \
|
273
|
+
"couch_object_timestamp_on_create?")
|
274
|
+
|
275
|
+
new_object_from_couch.instance_variable_set("@updated_at", \
|
276
|
+
updated_at) if eval("#{class_type}." \
|
277
|
+
"couch_object_timestamp_on_update?")
|
278
|
+
|
279
|
+
new_object_from_couch.
|
280
|
+
instance_variable_set("@belongs_to", belongs_to) \
|
281
|
+
if belongs_to
|
282
|
+
|
283
|
+
new_object_from_couch.couch_set_initial_state
|
284
|
+
|
285
|
+
# Returns the new object
|
286
|
+
new_object_from_couch
|
14
287
|
end
|
15
288
|
end
|
16
289
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
290
|
+
public
|
291
|
+
|
292
|
+
#
|
293
|
+
# Accessors for instance variables specific to the persistable
|
294
|
+
# CouchObject
|
295
|
+
#
|
296
|
+
attr_reader :id,
|
297
|
+
:revision,
|
298
|
+
:updated_at,
|
299
|
+
:created_at
|
300
|
+
|
301
|
+
#
|
302
|
+
# Returns:
|
303
|
+
# * +true+ if the object hasn't been saved
|
304
|
+
# * +false+ if the object has previously been stored or is loaded from the
|
305
|
+
# document store
|
306
|
+
#
|
307
|
+
def new?
|
308
|
+
id.nil? || revision.nil?
|
309
|
+
end
|
310
|
+
|
311
|
+
#
|
312
|
+
# Stores the initial value of the instance to a variable
|
313
|
+
# for later reference by the +unsaved_changes?+ method
|
314
|
+
#
|
315
|
+
def couch_set_initial_state
|
316
|
+
# For the unsaved_changes? instance method to work, we have to
|
317
|
+
# supply a snapshot of what the fresh object looked like.
|
318
|
+
# BUT ONLY if the user has activated the smart_save option
|
319
|
+
if use_smart_save
|
320
|
+
|
321
|
+
@couch_initial_load = true
|
322
|
+
@couch_object_original_state = to_json
|
323
|
+
@couch_initial_load = false
|
324
|
+
|
23
325
|
end
|
24
|
-
response
|
25
326
|
end
|
327
|
+
|
328
|
+
#
|
329
|
+
# Forces the instance object into smart save mode
|
330
|
+
#
|
331
|
+
def couch_force_smart_save
|
332
|
+
def self.use_smart_save
|
333
|
+
true
|
334
|
+
end
|
335
|
+
end
|
26
336
|
|
27
|
-
#
|
28
|
-
|
29
|
-
|
337
|
+
#
|
338
|
+
# Saves the object to the db_uri supplied, or if not set, to the
|
339
|
+
# location the object has previously been saved to.
|
340
|
+
#
|
341
|
+
# Takes:
|
342
|
+
# * +db_uri+ as string which is the location of the database:
|
343
|
+
# Example: http://localhost:5984/mydb
|
344
|
+
#
|
345
|
+
# Raises:
|
346
|
+
# * CouchObject::Errors::NoDatabaseLocationSet error
|
347
|
+
# if the object doesn't have a previously set location and
|
348
|
+
# the +db_uri+ is nil
|
349
|
+
#
|
350
|
+
# Returns:
|
351
|
+
# * Hash with the id and revision:
|
352
|
+
# {:id => "1234", :revision => "ABC123"}
|
353
|
+
#
|
354
|
+
# Sub methods might raise:
|
355
|
+
# * CouchObject::Errors::DatabaseSaveFailed if the save fails
|
356
|
+
#
|
357
|
+
def save(db_uri = location)
|
358
|
+
# if the location hasn't been set, set it
|
359
|
+
@location ||= db_uri
|
360
|
+
|
361
|
+
# Raises an error if the location variable hasn't been set
|
362
|
+
raise CouchObject::Errors::NoDatabaseLocationSet unless location
|
363
|
+
|
364
|
+
# If it's belongs_to relationships haven't been saved
|
365
|
+
# then it has be done first.
|
366
|
+
# Saving the master will also automatically save the child
|
367
|
+
performed_save = false
|
368
|
+
|
369
|
+
# If the belongs_to relationships haven't already been loaded,
|
370
|
+
# there is reason to believe that:
|
371
|
+
# * The object is new and doesn't have any relation set
|
372
|
+
# * The object already knows about it's belongs_to relations
|
373
|
+
# and doesn't need aditional information about them for saving
|
374
|
+
# We therefore deactivate the loading of belongs_to relations
|
375
|
+
original_state_of_load_belongs_to_relations = \
|
376
|
+
@do_not_load_belongs_to_relations
|
377
|
+
@do_not_load_belongs_to_relations = true
|
378
|
+
|
379
|
+
belongs_to.each do |what_it_belongs_to|
|
380
|
+
master_class = self.send(what_it_belongs_to)
|
381
|
+
unless master_class == nil || !master_class.new?
|
382
|
+
master_class.save
|
383
|
+
performed_save = true
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
# Reset the do_not_load_belongs_to_relations variable to its
|
388
|
+
# original state
|
389
|
+
@do_not_load_belongs_to_relations = \
|
390
|
+
original_state_of_load_belongs_to_relations
|
391
|
+
|
392
|
+
# If none of the master classes were saved, meaning they weren't new
|
393
|
+
# or didn't exist, then we have to manually save this object.
|
394
|
+
couch_perform_save unless performed_save
|
395
|
+
|
396
|
+
{:id => @id, :revision => @revision}
|
397
|
+
|
398
|
+
end
|
399
|
+
protected
|
400
|
+
def couch_perform_save
|
401
|
+
perform_callback(:before_save)
|
402
|
+
|
403
|
+
# Go... action
|
404
|
+
the_return_value = new? ? couch_create : couch_update
|
405
|
+
|
406
|
+
# Save all the has_many relations
|
407
|
+
# But only the has_many relations that have already
|
408
|
+
# been loaded! No need to load the relations from
|
409
|
+
# the db to save them back again!
|
410
|
+
state_before_wants_to_load_relations = @do_not_load_has_many_relations
|
411
|
+
@do_not_load_has_many_relations = true
|
412
|
+
|
413
|
+
# We thread the save process in case the relations do
|
414
|
+
# some funky time consuming stuff in their call backs
|
415
|
+
threads = []
|
416
|
+
has_many.each do |thing_it_has_many_of|
|
417
|
+
self.send(thing_it_has_many_of).each do |related_object|
|
418
|
+
threads << Thread.new(related_object) do |object_to_save|
|
419
|
+
object_to_save.save(location)
|
420
|
+
end
|
421
|
+
end
|
422
|
+
end
|
423
|
+
threads.each {|thr| thr.join}
|
424
|
+
|
425
|
+
# Save all the has_one relations
|
426
|
+
has_one.each do |thing_it_has_one_of|
|
427
|
+
related_object = self.send(thing_it_has_one_of)
|
428
|
+
|
429
|
+
unless related_object == nil
|
430
|
+
related_object.save(location)
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
# Reset the do_not_load_has_many_relations variable
|
435
|
+
# to it's original state
|
436
|
+
@do_not_load_has_many_relations = state_before_wants_to_load_relations
|
437
|
+
|
438
|
+
perform_callback(:after_save)
|
439
|
+
|
440
|
+
return the_return_value
|
441
|
+
end
|
442
|
+
|
443
|
+
#
|
444
|
+
# Saves the class as a new document in the database
|
445
|
+
#
|
446
|
+
# Returns:
|
447
|
+
# * CouchObject::Response
|
448
|
+
#
|
449
|
+
# Raises:
|
450
|
+
# * CouchObject::Errors::DatabaseSaveFailed if the save fails
|
451
|
+
#
|
452
|
+
def couch_create
|
453
|
+
perform_callback(:before_create)
|
454
|
+
|
455
|
+
db = CouchObject::Database.open(location)
|
456
|
+
|
457
|
+
json_value = self.to_json
|
458
|
+
|
459
|
+
unless (response = db.post("", json_value)).to_document["error"]
|
460
|
+
response_document = response.to_document
|
461
|
+
@id = response_document.id
|
462
|
+
@revision = response_document.revision
|
463
|
+
@created_at = Time.now if self.
|
464
|
+
class::couch_object_timestamp_on_create?
|
465
|
+
@updated_at = Time.now if self.
|
466
|
+
class::couch_object_timestamp_on_update?
|
467
|
+
|
468
|
+
# If the user has activated smart_save, we should set the state
|
469
|
+
# to the new contents of this instance!
|
470
|
+
@couch_object_original_state = json_value if use_smart_save
|
471
|
+
|
472
|
+
perform_callback(:after_create)
|
473
|
+
|
474
|
+
# Returns a hash with the ID and Revision
|
475
|
+
{:id => @id, :revision => @revision}
|
476
|
+
|
477
|
+
else
|
478
|
+
|
479
|
+
raise CouchObject::Errors::DatabaseSaveFailed, "The document " + \
|
480
|
+
"couldn't be created.\n" + \
|
481
|
+
"CouchDB reported: #{response.to_document["error"]}"
|
482
|
+
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
486
|
+
#
|
487
|
+
# Updates a document in the database based on the id and revision
|
488
|
+
#
|
489
|
+
# Returns:
|
490
|
+
# * CouchObject::Response
|
491
|
+
#
|
492
|
+
# Raises:
|
493
|
+
# * CouchObject::Errors::DatabaseSaveFailed if the update fails
|
494
|
+
#
|
495
|
+
def couch_update
|
496
|
+
|
497
|
+
# Only save if it has unsaved changes!
|
498
|
+
if unsaved_changes?
|
499
|
+
|
500
|
+
perform_callback(:before_update)
|
501
|
+
|
502
|
+
# Please notice the following:
|
503
|
+
# The response from CouchDB only includes the revision number
|
504
|
+
# and the ID so the updated_at value in the document store
|
505
|
+
# differs from the updated_at value in the class
|
506
|
+
@updated_at = Time.now if self.
|
507
|
+
class::couch_object_timestamp_on_update?
|
508
|
+
|
509
|
+
db = CouchObject::Database.open(location)
|
510
|
+
|
511
|
+
json_value = self.to_json
|
512
|
+
|
513
|
+
unless (response = db.put(id, json_value)).to_document["error"]
|
514
|
+
|
515
|
+
@revision = response.to_document.revision
|
516
|
+
|
517
|
+
# If the user has activated smart_save, we should set the state
|
518
|
+
# to the new contents of this instance!
|
519
|
+
@couch_object_original_state = json_value if use_smart_save
|
520
|
+
|
521
|
+
perform_callback(:after_update)
|
522
|
+
|
523
|
+
# Returns a hash with the ID and Revision
|
524
|
+
{:id => @id, :revision => @revision}
|
525
|
+
|
526
|
+
else
|
527
|
+
|
528
|
+
raise CouchObject::Errors::DatabaseSaveFailed, "The document " + \
|
529
|
+
"couldn't be updated.\n" + \
|
530
|
+
"The reason might be a revision number conflict.\n" + \
|
531
|
+
"CouchDB reported: #{response.to_document["reason"]}"
|
532
|
+
|
533
|
+
end
|
534
|
+
|
535
|
+
|
536
|
+
end
|
537
|
+
|
30
538
|
end
|
539
|
+
|
540
|
+
|
541
|
+
public
|
542
|
+
#
|
543
|
+
# Classes WITH smart_save activated:
|
544
|
+
# Any instance should be able to know if it has unsaved changes or not.
|
545
|
+
# When an instance is loaded from the DB it creates a snapshot of what
|
546
|
+
# its variables contain. Based on a comparison between the snapshot
|
547
|
+
# and the contents of the instance this method returns true or false.
|
548
|
+
#
|
549
|
+
# Classes WITHOUT smart_save activated:
|
550
|
+
# Will always return true regardless of what state it is in
|
551
|
+
#
|
552
|
+
# A new object will always return true
|
553
|
+
#
|
554
|
+
# Returns:
|
555
|
+
# * true: if it has changes that haven't been saved to the database
|
556
|
+
# * fase: if the nothing has changed since it was loaded from the
|
557
|
+
# database.
|
558
|
+
#
|
559
|
+
def unsaved_changes?
|
560
|
+
return true if new?
|
561
|
+
return true unless use_smart_save
|
562
|
+
|
563
|
+
@couch_object_original_state == self.to_json ? false : true
|
564
|
+
|
565
|
+
end
|
566
|
+
|
567
|
+
#
|
568
|
+
# Any instance should be able to delete itself
|
569
|
+
#
|
570
|
+
# Takes:
|
571
|
+
# * +db_uri+ if not set in the location variable
|
572
|
+
#
|
573
|
+
# Returns:
|
574
|
+
# * true on success
|
575
|
+
# * false on failure
|
576
|
+
#
|
577
|
+
# Note:
|
578
|
+
# * it also deletes all has_many relations from the database
|
579
|
+
# * it removes itself from object it belongs to
|
580
|
+
#
|
581
|
+
# Raises:
|
582
|
+
# * CouchObject::Errors::NoDatabaseLocationSet if +db_uri+
|
583
|
+
# is blank AND has not been set on class level
|
584
|
+
#
|
585
|
+
def delete(db_uri = location)
|
586
|
+
|
587
|
+
perform_callback(:before_delete)
|
588
|
+
|
589
|
+
unless new?
|
590
|
+
# Raises an error if the location variable hasn't been set
|
591
|
+
raise CouchObject::Errors::NoDatabaseLocationSet unless db_uri
|
592
|
+
|
593
|
+
db = CouchObject::Database.open(db_uri)
|
594
|
+
|
595
|
+
# Removes itself from the database
|
596
|
+
db.delete(id, revision)
|
597
|
+
end
|
598
|
+
|
599
|
+
# Remove all relations
|
600
|
+
has_many.each do |what_it_has|
|
601
|
+
self.send(what_it_has).dup.each do |related_object|
|
602
|
+
related_object.delete
|
603
|
+
end
|
604
|
+
end
|
605
|
+
|
606
|
+
# Remove the relationship with it's has many master object
|
607
|
+
belongs_to.each do |what_it_belongs_to|
|
608
|
+
self_belongs_to_classtype = what_it_belongs_to
|
609
|
+
|
610
|
+
self_belongs_to_classtype_as = self.
|
611
|
+
send("belongs_to_#{what_it_belongs_to}_as")
|
612
|
+
|
613
|
+
self_belongs_to_class = self.send(self_belongs_to_classtype) unless \
|
614
|
+
self_belongs_to_classtype.nil?
|
615
|
+
|
616
|
+
self_belongs_to_class.end_relationsship_with(self,
|
617
|
+
self_belongs_to_classtype_as) unless self_belongs_to_class.nil?
|
618
|
+
|
619
|
+
# Remove the relationship with it's belongs_to master from itself
|
620
|
+
remove_call = "#{self_belongs_to_classtype.to_s}" + \
|
621
|
+
"_without_call_back="
|
622
|
+
self.send(remove_call.to_sym, nil) if self_belongs_to_classtype
|
623
|
+
|
624
|
+
end
|
625
|
+
|
626
|
+
# Reset itself
|
627
|
+
@id = nil
|
628
|
+
@revision = nil
|
629
|
+
@location = nil
|
630
|
+
|
631
|
+
perform_callback(:after_delete)
|
632
|
+
|
633
|
+
true
|
31
634
|
|
32
|
-
# the Couch document id of this object
|
33
|
-
def id
|
34
|
-
@id
|
35
635
|
end
|
36
636
|
|
37
|
-
#
|
637
|
+
#
|
638
|
+
# Breaks relations if existing
|
639
|
+
#
|
640
|
+
# Takes:
|
641
|
+
# * +undesired_object+ as a reference to the object the
|
642
|
+
# relationship should be broken with
|
643
|
+
# * +which_is_stored_as+ (string) which is what the relation
|
644
|
+
# is stored as.
|
645
|
+
#
|
646
|
+
def end_relationsship_with(undersired_object, which_is_stored_as)
|
647
|
+
self.send(which_is_stored_as.to_sym).perform_remove(undersired_object)
|
648
|
+
end
|
649
|
+
|
650
|
+
#
|
651
|
+
# Sets the location variable manually
|
652
|
+
#
|
653
|
+
# Takes:
|
654
|
+
# * +db_uri+ as string which is the location of the database:
|
655
|
+
# Example: http://localhost:5984/mydb
|
656
|
+
#
|
657
|
+
def set_location=(db_uri)
|
658
|
+
@location = db_uri == "" ? nil : db_uri
|
659
|
+
end
|
660
|
+
alias set_storage_location= set_location=
|
661
|
+
|
662
|
+
#
|
663
|
+
# serializes this object into JSON
|
664
|
+
#
|
665
|
+
# Returns:
|
666
|
+
# * The values of the class in json format
|
667
|
+
#
|
668
|
+
# Example
|
669
|
+
# {"class":"Bike","attributes":{"wheels":2}}
|
670
|
+
#
|
38
671
|
def to_json
|
39
|
-
|
40
|
-
|
672
|
+
parameters = {}
|
673
|
+
|
674
|
+
parameters["class"] = self.class
|
675
|
+
|
676
|
+
if respond_to?(:to_couch)
|
677
|
+
parameters["attributes"] = self.to_couch
|
678
|
+
else
|
679
|
+
# Find all the instance variables and add them to the
|
680
|
+
# attributes parameter
|
681
|
+
p_attributes = {}
|
682
|
+
instance_variables = \
|
683
|
+
self.instance_variables - ["@location",
|
684
|
+
"@created_at",
|
685
|
+
"@updated_at",
|
686
|
+
"@id",
|
687
|
+
"@revision",
|
688
|
+
"@do_not_load_has_many_relations",
|
689
|
+
"@do_not_load_belongs_to_relations",
|
690
|
+
"@couch_object_original_state",
|
691
|
+
"@couch_initial_load",
|
692
|
+
"@belongs_to"]
|
693
|
+
|
694
|
+
# We also have to remove all the objects that are related
|
695
|
+
# through belongs_to and has_many relations. They have to be called
|
696
|
+
# saved separately
|
697
|
+
has.each do |thing_it_has|
|
698
|
+
instance_variables = instance_variables - \
|
699
|
+
["@couch_object_#{thing_it_has.to_s}"]
|
700
|
+
end
|
701
|
+
|
702
|
+
belongs_to.each do |things_it_belongs_to|
|
703
|
+
instance_variables = instance_variables - \
|
704
|
+
["@couch_object_#{things_it_belongs_to.to_s}"]
|
705
|
+
end
|
706
|
+
|
707
|
+
instance_variables.each do |var|
|
708
|
+
p_attributes[var[1..(var.length)]] = self.instance_variable_get(var)
|
709
|
+
end
|
710
|
+
parameters["attributes"] = p_attributes
|
711
|
+
end
|
712
|
+
|
713
|
+
parameters["updated_at"] = Time.now \
|
714
|
+
if self.class::couch_object_timestamp_on_update?
|
715
|
+
|
716
|
+
unless new?
|
717
|
+
parameters["_id"] = id
|
718
|
+
parameters["_rev"] = revision
|
719
|
+
parameters["created_at"] = created_at \
|
720
|
+
if self.class::couch_object_timestamp_on_create?
|
721
|
+
else
|
722
|
+
parameters["created_at"] = Time.now \
|
723
|
+
if self.class::couch_object_timestamp_on_create?
|
724
|
+
end
|
725
|
+
|
726
|
+
# If it is in belongs_to relationship(s), then that fact
|
727
|
+
# has to be stored in the database
|
728
|
+
if belongs_to != [] and !@couch_initial_load # the couch_initial_load
|
729
|
+
# is to get the initial
|
730
|
+
# state of the object
|
731
|
+
# without loading the
|
732
|
+
# belongs_to relations
|
733
|
+
# which would start an
|
734
|
+
# infinite loop.
|
735
|
+
# NOTE: this is only for
|
736
|
+
# cases where smart_save
|
737
|
+
# has been activated.
|
738
|
+
|
739
|
+
# No reason to load the belongs_to relations if they haven't
|
740
|
+
# already been loaded from the database!
|
741
|
+
original_state_of_load_belongs_to_relations = \
|
742
|
+
@do_not_load_belongs_to_relations
|
743
|
+
@do_not_load_belongs_to_relations = true
|
744
|
+
|
745
|
+
what_it_belongs_to = {}
|
746
|
+
self.send(:belongs_to).each do |relation|
|
747
|
+
as_what = self.send("belongs_to_#{relation}_as")
|
748
|
+
object_it_belongs_to = self.send(relation)
|
749
|
+
|
750
|
+
# Unless the relation is unset, in which case it will be of
|
751
|
+
# type NilClass, set it.
|
752
|
+
what_it_belongs_to[as_what.to_s] = object_it_belongs_to.id || "new" \
|
753
|
+
unless object_it_belongs_to.class == NilClass
|
754
|
+
end
|
755
|
+
|
756
|
+
# Reset the value so they are loaded the next time when needed
|
757
|
+
# if that is what the user wants.
|
758
|
+
@do_not_load_belongs_to_relations = \
|
759
|
+
original_state_of_load_belongs_to_relations
|
760
|
+
|
761
|
+
|
762
|
+
# We have to make sure the @belongs_to variable contains all changes
|
763
|
+
# and all the original values for the keys that haven't changed/
|
764
|
+
# relations that haven't been loaded
|
765
|
+
original_belongs_to = @belongs_to || {}
|
766
|
+
@belongs_to = what_it_belongs_to
|
767
|
+
times_through = 1
|
768
|
+
|
769
|
+
# LOOP 1... see below for problem description
|
770
|
+
original_belongs_to.each_pair do |key, value|
|
771
|
+
times_through += 1
|
772
|
+
@belongs_to[key.to_s] = value unless @belongs_to[key.to_s]
|
773
|
+
end
|
774
|
+
|
775
|
+
# FIXME:
|
776
|
+
# Now... this is a really hacky way to solve this problem
|
777
|
+
# and should be improved... Feel free to come up with sollutions
|
778
|
+
# Case:
|
779
|
+
# If it has a belongs_to relationship that is new and therefore
|
780
|
+
# doesn't have an ID it would normally be written in the @belongs_to
|
781
|
+
# variable as nil. The problem is that if self has previously
|
782
|
+
# been saved with another parent object this ID would still come
|
783
|
+
# through in the @belongs_to variable updater (see "LOOP 1" above).
|
784
|
+
# We therefore assign the ID "new" to all unsaved relations, which we
|
785
|
+
# now have to nilify. If we don't the smart save wont work for this
|
786
|
+
# type of cases.
|
787
|
+
|
788
|
+
end
|
789
|
+
|
790
|
+
parameters["belongs_to"] = @belongs_to
|
791
|
+
parameters.delete("belongs_to") if @belongs_to == {} or @belongs_to == nil
|
792
|
+
|
793
|
+
# if @couch_initial_load && @belongs_to
|
794
|
+
|
795
|
+
begin
|
796
|
+
parameters.to_json
|
797
|
+
rescue JSON::GeneratorError
|
798
|
+
# All strings aren't encoded properly, so we have to force them into
|
799
|
+
# UTF-8.
|
800
|
+
# FIXME: The kconv library has some weird artefacts though where
|
801
|
+
# a lot of Norwegian (Scandianavian?) letters get turned into
|
802
|
+
# asian characters of some sort!
|
803
|
+
CouchObject::Utils::decode_strings(parameters).to_json
|
804
|
+
end
|
805
|
+
|
806
|
+
end
|
807
|
+
|
808
|
+
|
809
|
+
|
810
|
+
#
|
811
|
+
# Sometimes you might want to add an object to
|
812
|
+
# a has_many relation without interacting with the other relations at all.
|
813
|
+
# In cases like that, when loading all the relations would just
|
814
|
+
# cause unnecessary traffic to the database, you can tell the object
|
815
|
+
# not to load it has_many relations using this method
|
816
|
+
#
|
817
|
+
def do_not_load_has_many_relations
|
818
|
+
@do_not_load_has_many_relations = true
|
819
|
+
end
|
820
|
+
def do_load_has_many_relations
|
821
|
+
@do_not_load_has_many_relations = false
|
822
|
+
end
|
823
|
+
alias do_not_load_has_one_relations do_not_load_has_many_relations
|
824
|
+
alias do_load_has_one_relations do_load_has_many_relations
|
825
|
+
alias do_not_load_has_one_relation do_not_load_has_many_relations
|
826
|
+
alias do_load_has_one_relation do_load_has_many_relations
|
827
|
+
|
828
|
+
#
|
829
|
+
# If you need to access the belongs_to variable without loading the
|
830
|
+
# relation if it hasn't already been loaded, you can call the instance
|
831
|
+
# method +do_not_load_belongs_to_relations+. To reactivate loading so
|
832
|
+
# the relation is loaded the next time it is needed, call the instance
|
833
|
+
# method +do_load_belongs_to_relations+.
|
834
|
+
#
|
835
|
+
def do_not_load_belongs_to_relations
|
836
|
+
@do_not_load_belongs_to_relations = true
|
41
837
|
end
|
838
|
+
def do_load_belongs_to_relations
|
839
|
+
@do_not_load_belongs_to_relations = false
|
840
|
+
end
|
841
|
+
alias do_not_load_has_many_relation do_not_load_has_many_relations
|
842
|
+
alias do_load_belongs_to_relation do_load_belongs_to_relations
|
843
|
+
|
844
|
+
|
845
|
+
protected
|
846
|
+
#
|
847
|
+
# Loads has_many relations
|
848
|
+
#
|
849
|
+
def couch_load_has_many_relations(which_relation)
|
850
|
+
# If it is a new and unsaved object it wont have
|
851
|
+
# relations in the DB. Return a blank array
|
852
|
+
results = CouchObject::Persistable::HasManyRelation.new(self)
|
853
|
+
|
854
|
+
# If it is loading a has_one relation, the relation name has to
|
855
|
+
# be changed
|
856
|
+
which_relation = which_relation[8..-1] \
|
857
|
+
if which_relation[0..7] == "has_one_"
|
858
|
+
|
859
|
+
return results if new? || @do_not_load_has_many_relations
|
42
860
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
861
|
+
view_name = "couch_object_has_many_relations"
|
862
|
+
|
863
|
+
begin
|
864
|
+
|
865
|
+
new_objects = self.class.
|
866
|
+
get_from_view("_view/#{view_name}/related_documents", \
|
867
|
+
{:key => [self.id, which_relation],
|
868
|
+
:db_uri => location} )
|
869
|
+
|
870
|
+
# Disable the call back function of HasManyRelations so we don't
|
871
|
+
# get an infinite loop
|
872
|
+
results.disable_call_back_on_add
|
873
|
+
|
874
|
+
new_objects.each do |new_object|
|
875
|
+
results << new_object
|
876
|
+
end
|
877
|
+
|
878
|
+
# Reanable the call back function again so it works like normal
|
879
|
+
# from now on
|
880
|
+
results.enable_call_back_on_add
|
881
|
+
|
882
|
+
rescue CouchObject::Errors::MissingView, CouchObject::Errors::CouchDBError
|
883
|
+
|
884
|
+
# The view doesn't exist. It means this is the first
|
885
|
+
# time this script is used for a given database, or the user
|
886
|
+
# has deleted the view
|
887
|
+
view_code_query = JSON.unparse(
|
888
|
+
{
|
889
|
+
"_id" => "_design/#{view_name}",
|
890
|
+
"language" => "text/javascript",
|
891
|
+
"views" =>
|
892
|
+
{
|
893
|
+
"related_documents" => "function(doc){if (doc.belongs_to){" + \
|
894
|
+
"for (var i in doc.belongs_to) {map([doc.belongs_to[i],i], " + \
|
895
|
+
"doc);}}}"
|
896
|
+
}
|
897
|
+
}
|
898
|
+
)
|
899
|
+
|
900
|
+
db = CouchObject::Database.open(location)
|
901
|
+
|
902
|
+
if (response = db.put("_design%2F#{view_name}", \
|
903
|
+
view_code_query))
|
904
|
+
results = couch_load_has_many_relations(which_relation)
|
905
|
+
else
|
906
|
+
raise CouchObject::Errors::StandardError, "Couldn't create the view..."
|
907
|
+
end
|
47
908
|
end
|
48
|
-
|
909
|
+
|
910
|
+
return results
|
49
911
|
end
|
50
912
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
913
|
+
#
|
914
|
+
# Loads the belongs_to relation if the class has a previously
|
915
|
+
# save relation
|
916
|
+
#
|
917
|
+
def couch_load_belongs_to_relation(that_is_called)
|
918
|
+
|
919
|
+
return nil if new? || @do_not_load_belongs_to_relations
|
920
|
+
|
921
|
+
# If it doesn't have a belongs to ID then there is no
|
922
|
+
# related object in the database
|
923
|
+
if @belongs_to && @belongs_to[that_is_called]
|
924
|
+
|
925
|
+
# If it is a new and unsaved object it wont have
|
926
|
+
# relations in the DB. Return a blank array
|
927
|
+
return nil if new?
|
928
|
+
|
929
|
+
# Raises an error if the location variable hasn't been set
|
930
|
+
raise CouchObject::Errors::NoDatabaseLocationSet unless location
|
931
|
+
|
932
|
+
db = CouchObject::Database.open(location)
|
933
|
+
|
934
|
+
class_self_belongs_to = self.
|
935
|
+
class.get_by_id(@belongs_to[that_is_called])
|
936
|
+
|
937
|
+
# We have to add self to the other end of the relation
|
938
|
+
# Adds itself to the relationship
|
939
|
+
masters_relation = class_self_belongs_to.send(that_is_called)
|
940
|
+
|
941
|
+
if masters_relation.class == CouchObject::Persistable::HasManyRelation
|
942
|
+
# it is a has_many relations
|
943
|
+
# remove the object in the relation that is the freshly loaded
|
944
|
+
# copy of self, and then add self instead.
|
945
|
+
masters_relation.each do |has_one|
|
946
|
+
if has_one.id == self.id && has_one.revision == self.revision
|
947
|
+
masters_relation.perform_remove(has_one)
|
948
|
+
break
|
949
|
+
end
|
950
|
+
end
|
951
|
+
masters_relation.disable_call_back_on_add
|
952
|
+
masters_relation << self
|
953
|
+
masters_relation.enable_call_back_on_add
|
954
|
+
else
|
955
|
+
# it is a has_one relation, just add itself
|
956
|
+
class_self_belongs_to.send("#{that_is_called}=".to_sym, self)
|
957
|
+
end
|
958
|
+
|
959
|
+
return class_self_belongs_to
|
960
|
+
|
961
|
+
else
|
962
|
+
nil
|
55
963
|
end
|
56
|
-
alias_method :to_s, :message
|
57
964
|
end
|
965
|
+
|
966
|
+
#
|
967
|
+
# Performs callbacks before and after these events:
|
968
|
+
# * create
|
969
|
+
# * update
|
970
|
+
# * save
|
971
|
+
# * delete
|
972
|
+
#
|
973
|
+
def perform_callback(the_callback)
|
974
|
+
self.send(the_callback) if self.respond_to?(the_callback)
|
975
|
+
end
|
58
976
|
end
|
59
977
|
end
|