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.
Files changed (63) hide show
  1. data/History.txt +10 -0
  2. data/Manifest.txt +30 -6
  3. data/README.txt +580 -42
  4. data/TODO +2 -2
  5. data/config/hoe.rb +1 -1
  6. data/lib/couch_object.rb +7 -2
  7. data/lib/couch_object/database.rb +19 -34
  8. data/lib/couch_object/document.rb +13 -6
  9. data/lib/couch_object/error_classes.rb +110 -0
  10. data/lib/couch_object/persistable.rb +954 -36
  11. data/lib/couch_object/persistable/has_many_relations_array.rb +91 -0
  12. data/lib/couch_object/persistable/meta_classes.rb +568 -0
  13. data/lib/couch_object/persistable/overloaded_methods.rb +209 -0
  14. data/lib/couch_object/server.rb +1 -1
  15. data/lib/couch_object/utils.rb +44 -0
  16. data/lib/couch_object/version.rb +1 -1
  17. data/lib/couch_object/view.rb +129 -6
  18. data/script/console +0 -0
  19. data/script/destroy +0 -0
  20. data/script/generate +0 -0
  21. data/script/txt2html +0 -0
  22. data/spec/database_spec.rb +23 -31
  23. data/spec/database_spec.rb.orig +173 -0
  24. data/spec/document_spec.rb +21 -3
  25. data/spec/integration/database_integration_spec.rb +46 -15
  26. data/spec/integration/integration_helper.rb +3 -3
  27. data/spec/persistable/callback.rb +44 -0
  28. data/spec/persistable/callback_spec.rb +44 -0
  29. data/spec/persistable/cloning.rb +77 -0
  30. data/spec/persistable/cloning_spec.rb +77 -0
  31. data/spec/persistable/comparing_objects.rb +350 -0
  32. data/spec/persistable/comparing_objects_spec.rb +350 -0
  33. data/spec/persistable/deleting.rb +113 -0
  34. data/spec/persistable/deleting_spec.rb +113 -0
  35. data/spec/persistable/error_messages.rb +32 -0
  36. data/spec/persistable/error_messages_spec.rb +32 -0
  37. data/spec/persistable/loading.rb +339 -0
  38. data/spec/persistable/loading_spec.rb +339 -0
  39. data/spec/persistable/new_methods.rb +70 -0
  40. data/spec/persistable/new_methods_spec.rb +70 -0
  41. data/spec/persistable/persistable_helper.rb +194 -0
  42. data/spec/persistable/relations.rb +470 -0
  43. data/spec/persistable/relations_spec.rb +470 -0
  44. data/spec/persistable/saving.rb +137 -0
  45. data/spec/persistable/saving_spec.rb +137 -0
  46. data/spec/persistable/setting_storage_location.rb +65 -0
  47. data/spec/persistable/setting_storage_location_spec.rb +65 -0
  48. data/spec/persistable/timestamps.rb +76 -0
  49. data/spec/persistable/timestamps_spec.rb +76 -0
  50. data/spec/persistable/unsaved_changes.rb +211 -0
  51. data/spec/persistable/unsaved_changes_spec.rb +211 -0
  52. data/spec/server_spec.rb +5 -5
  53. data/spec/utils_spec.rb +60 -0
  54. data/spec/view_spec.rb +40 -7
  55. data/website/index.html +22 -7
  56. data/website/index.txt +13 -5
  57. metadata +93 -61
  58. data/bin/couch_ruby_view_requestor +0 -81
  59. data/lib/couch_object/model.rb +0 -5
  60. data/lib/couch_object/proc_condition.rb +0 -14
  61. data/spec/model_spec.rb +0 -5
  62. data/spec/persistable_spec.rb +0 -91
  63. data/spec/proc_condition_spec.rb +0 -26
data/TODO CHANGED
@@ -1,4 +1,4 @@
1
1
  - Expand on View class
2
- - Do dot notation for document attributes in Database#filter
3
2
 
4
- - CouchObject::Model for more domain specific/clearer way to model Couch docs in ruby
3
+ - CouchObject::Model for more domain specific/clearer way to model Couch docs in ruby
4
+
@@ -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
@@ -11,12 +11,16 @@ rescue LoadError
11
11
  end
12
12
 
13
13
  require 'json/add/core'
14
- require "ruby2ruby"
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!(uri, dbname)
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:8888/foo
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:8888/foo
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 call
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 call
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 call
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.put("/#{Utils.join_url(@dbname, path)}", payload)).parse
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 database path
77
- # so calling with with "bar" as the path in the "foo_db" database will call
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.delete("/#{Utils.join_url(@dbname, path)}")).parse
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
- @id = response.to_document.id
83
- @revision = response.to_document.revision
84
- response
82
+ apply_response(response)
85
83
  end
86
84
 
87
85
  def update(database)
88
- response = database.put(id, self.to_json("_rev" => revision))
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
- def self.included(klazz)
4
- klazz.extend(ClassMethods)
5
- end
6
-
62
+ module Persistable
63
+
7
64
  module ClassMethods
8
- # Get a document from +db_uri+ with +id+ as the document id
9
- def get_by_id(db_uri, id)
10
- raise NoFromCouchMethodError unless respond_to?(:from_couch)
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
- self.send(:from_couch, response["attributes"])
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
- # Save the object to +db_uri+
18
- def save(db_uri)
19
- db = CouchObject::Database.open(db_uri)
20
- response = db.post("", self.to_json)
21
- unless response.empty?
22
- @id = response["_id"]
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
- # Is this a new unsaved object?
28
- def new?
29
- id.nil?
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
- # serializes this object, based on its #to_couch method, into JSON
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
- raise NoToCouchMethodError unless respond_to?(:to_couch)
40
- {"class" => self.class, "attributes" => self.to_couch}.to_json
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
- class NoToCouchMethodError < StandardError
44
- def message
45
- "You need to define a #to_couch method that returns a hash of the " +
46
- "attributes you want to persist"
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
- alias_method :to_s, :message
909
+
910
+ return results
49
911
  end
50
912
 
51
- class NoFromCouchMethodError < StandardError
52
- def message
53
- "You need to define a from_couch(attrs) class method that maps attrs " +
54
- "to your class instance"
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