offroad 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/LICENSE +674 -674
  2. data/README.rdoc +29 -29
  3. data/Rakefile +75 -75
  4. data/TODO +42 -42
  5. data/lib/app/models/offroad/group_state.rb +85 -85
  6. data/lib/app/models/offroad/mirror_info.rb +53 -53
  7. data/lib/app/models/offroad/model_state.rb +36 -36
  8. data/lib/app/models/offroad/received_record_state.rb +115 -115
  9. data/lib/app/models/offroad/sendable_record_state.rb +91 -91
  10. data/lib/app/models/offroad/system_state.rb +33 -33
  11. data/lib/cargo_streamer.rb +222 -222
  12. data/lib/controller_extensions.rb +74 -74
  13. data/lib/exceptions.rb +16 -16
  14. data/lib/migrate/20100512164608_create_offroad_tables.rb +72 -72
  15. data/lib/mirror_data.rb +376 -376
  16. data/lib/model_extensions.rb +378 -377
  17. data/lib/module_funcs.rb +94 -94
  18. data/lib/offroad.rb +41 -41
  19. data/lib/version.rb +3 -3
  20. data/lib/view_helper.rb +7 -7
  21. data/templates/offline.rb +36 -36
  22. data/templates/offline_database.yml +7 -7
  23. data/templates/offroad.yml +6 -6
  24. data/test/app_root/app/controllers/application_controller.rb +2 -2
  25. data/test/app_root/app/controllers/group_controller.rb +28 -28
  26. data/test/app_root/app/models/global_record.rb +10 -10
  27. data/test/app_root/app/models/group.rb +12 -12
  28. data/test/app_root/app/models/group_owned_record.rb +68 -68
  29. data/test/app_root/app/models/guest.rb +7 -7
  30. data/test/app_root/app/models/subrecord.rb +12 -12
  31. data/test/app_root/app/models/unmirrored_record.rb +4 -4
  32. data/test/app_root/app/views/group/download_down_mirror.html.erb +3 -3
  33. data/test/app_root/app/views/group/download_initial_down_mirror.html.erb +3 -3
  34. data/test/app_root/app/views/group/download_up_mirror.html.erb +5 -5
  35. data/test/app_root/app/views/layouts/mirror.html.erb +8 -8
  36. data/test/app_root/config/boot.rb +115 -115
  37. data/test/app_root/config/database-pg.yml +8 -8
  38. data/test/app_root/config/database.yml +5 -5
  39. data/test/app_root/config/environment.rb +24 -24
  40. data/test/app_root/config/environments/test.rb +17 -17
  41. data/test/app_root/config/offroad.yml +6 -6
  42. data/test/app_root/config/routes.rb +4 -4
  43. data/test/app_root/db/migrate/20100529235049_create_tables.rb +64 -64
  44. data/test/app_root/lib/common_hobo.rb +15 -15
  45. data/test/app_root/vendor/plugins/offroad/init.rb +2 -2
  46. data/test/functional/mirror_operations_test.rb +148 -148
  47. data/test/test_helper.rb +453 -453
  48. data/test/unit/app_state_tracking_test.rb +275 -275
  49. data/test/unit/cargo_streamer_test.rb +332 -332
  50. data/test/unit/global_data_test.rb +102 -102
  51. data/test/unit/group_controller_test.rb +152 -152
  52. data/test/unit/group_data_test.rb +442 -435
  53. data/test/unit/group_single_test.rb +136 -136
  54. data/test/unit/hobo_permissions_test.rb +57 -57
  55. data/test/unit/mirror_data_test.rb +1283 -1283
  56. data/test/unit/mirror_info_test.rb +31 -31
  57. data/test/unit/module_funcs_test.rb +37 -37
  58. data/test/unit/pathological_model_test.rb +62 -62
  59. data/test/unit/test_framework_test.rb +86 -86
  60. data/test/unit/unmirrored_data_test.rb +14 -14
  61. metadata +6 -8
@@ -1,377 +1,378 @@
1
- module Offroad
2
- module ModelExtensions
3
- OFFROAD_VALID_MODES = [:group_base, :group_owned, :group_single, :global]
4
- OFFROAD_GROUP_MODES = [:group_base, :group_owned, :group_single]
5
-
6
- def acts_as_offroadable(mode, opts = {})
7
- raise ModelError.new("You can only call acts_as_offroadable once per model") if acts_as_offroadable?
8
- raise ModelError.new("You must specify a mode, one of " + OFFROAD_VALID_MODES.map(&:inspect).join("/")) unless OFFROAD_VALID_MODES.include?(mode)
9
-
10
- set_internal_cattr :offroad_mode, mode
11
-
12
- case mode
13
- when :group_owned then
14
- raise ModelError.new("For :group_owned models, need to specify :parent") unless opts[:parent]
15
- assoc = reflect_on_association(opts.delete(:parent))
16
- raise ModelError.new("No such parent associaton") unless assoc
17
- raise ModelError.new("Parent association must be a belongs_to association") unless assoc.belongs_to?
18
- raise ModelError.new("Parent association must be to a group data model") unless assoc.klass.offroad_group_data?
19
-
20
- set_internal_cattr :offroad_parent_assoc, assoc
21
- Offroad::note_group_owned_model(self)
22
- when :group_single then
23
- Offroad::note_group_single_model(self)
24
- when :group_base then
25
- Offroad::note_group_base_model(self)
26
- when :global then
27
- Offroad::note_global_data_model(self)
28
- end
29
-
30
- # We should have deleted all the options from the hash by this point
31
- raise ModelError.new("Unknown or inapplicable option(s) specified") unless opts.size == 0
32
-
33
- case mode
34
- when :group_base then
35
- named_scope :owned_by_offroad_group, lambda { |group| { :conditions => { :id => group.id } } }
36
- named_scope :offline_groups, {
37
- :joins =>
38
- "INNER JOIN \"#{Offroad::GroupState.table_name}\" ON \"#{Offroad::GroupState.table_name}\".app_group_id = \"#{table_name}\".\"#{primary_key}\""
39
- }
40
- named_scope :online_groups, {
41
- :joins =>
42
- "LEFT JOIN \"#{Offroad::GroupState.table_name}\" ON \"#{Offroad::GroupState.table_name}\".app_group_id = \"#{table_name}\".\"#{primary_key}\"",
43
- :conditions =>
44
- "\"#{Offroad::GroupState.table_name}\".app_group_id IS NULL"
45
- }
46
- when :group_owned then
47
- named_scope :owned_by_offroad_group, lambda { |group| args_for_ownership_scope(group) }
48
- when :group_single then
49
- named_scope :owned_by_offroad_group, lambda { |group| {
50
- :conditions => (Offroad::GroupState.count > 0 && group == Offroad::GroupState.first.app_group) ? "1=1" : "1=0"
51
- } }
52
- end
53
-
54
- if offroad_group_data?
55
- include GroupDataInstanceMethods
56
- else
57
- include GlobalDataInstanceMethods
58
- end
59
- include CommonInstanceMethods
60
-
61
- before_destroy :before_mirrored_data_destroy
62
- after_destroy :after_mirrored_data_destroy
63
- before_save :before_mirrored_data_save
64
- after_save :after_mirrored_data_save
65
-
66
- if Object.const_defined?(:Hobo) and included_modules.include?(Hobo::Model)
67
- include HoboPermissionsInstanceMethods
68
- [
69
- [:create, :save],
70
- [:update, :save],
71
- [:destroy, :destroy]
72
- ].each do |perm_name, check_name|
73
- define_method "#{perm_name}_permitted_with_offroad_check?".to_sym do
74
- pre_check_passed?("before_mirrored_data_#{check_name}".to_sym) && send("#{perm_name}_permitted_without_offroad_check?")
75
- end
76
- alias_method_chain "#{perm_name}_permitted?", "offroad_check"
77
- end
78
- end
79
- end
80
-
81
- def offroad_model_state
82
- model_scope = Offroad::ModelState::for_model(self)
83
- return model_scope.first || model_scope.create
84
- end
85
-
86
- def acts_as_offroadable?
87
- respond_to? :offroad_mode
88
- end
89
-
90
- def safe_to_load_from_cargo_stream?
91
- acts_as_offroadable?
92
- end
93
-
94
- def offroad_group_base?
95
- acts_as_offroadable? && offroad_mode == :group_base
96
- end
97
-
98
- def offroad_group_data?
99
- acts_as_offroadable? && OFFROAD_GROUP_MODES.include?(offroad_mode)
100
- end
101
-
102
- def offroad_global_data?
103
- acts_as_offroadable? && offroad_mode == :global
104
- end
105
-
106
- private
107
-
108
- def set_internal_cattr(name, value)
109
- write_inheritable_attribute name, value
110
- class_inheritable_reader name
111
- end
112
-
113
- def args_for_ownership_scope(group)
114
- included_assocs = []
115
- conditions = []
116
- assoc_owner = self
117
- assoc = offroad_parent_assoc
118
- while true
119
- if assoc.klass.offroad_group_base?
120
- conditions << "\"#{assoc_owner.table_name}\".\"#{assoc.primary_key_name}\" = #{group.id}"
121
- break
122
- else
123
- conditions << "\"#{assoc_owner.table_name}\".\"#{assoc.primary_key_name}\" = \"#{assoc.klass.table_name}\".\"#{assoc.klass.primary_key}\""
124
- included_assocs << assoc
125
- assoc_owner = assoc.klass
126
- assoc = assoc.klass.offroad_parent_assoc
127
- end
128
- end
129
-
130
- includes = {}
131
- included_assocs.reverse.each do |assoc|
132
- includes = {assoc.name => includes}
133
- end
134
-
135
- return {:include => includes, :conditions => conditions.join(" AND ")}
136
- end
137
-
138
- module CommonInstanceMethods
139
- # Methods below this point are only to be used internally by Offroad
140
- # However, making all of them private would make using them from elsewhere troublesome
141
-
142
- # TODO Should put common save and destroy wrappers in here, with access to a method that checks if SRS needed
143
- # TODO That method should also be used in import_model_cargo instead of explicitly trying to find the srs
144
-
145
- #:nodoc:#
146
- def validate_changed_id_columns
147
- changes.each do |colname, arr|
148
- orig_val = arr[0]
149
- new_val = arr[1]
150
-
151
- raise DataError.new("Cannot change id of offroad-tracked records (orig #{orig_val.inspect}, new #{new_val.inspect}") if colname == self.class.primary_key && orig_val != nil
152
-
153
- # FIXME : Use association reflection instead
154
- next unless colname.end_with? "_id"
155
- accessor_name = colname[0, colname.size-3]
156
- next unless respond_to? accessor_name
157
- obj = send(accessor_name)
158
-
159
- raise DataError.new("Mirrored data cannot hold a foreign key to unmirrored data") unless obj.class.acts_as_offroadable?
160
-
161
- if !new_record? and offroad_mode == :group_owned and colname == offroad_parent_assoc.primary_key_name
162
- # obj is our parent
163
- # FIXME: What if we can't find orig_val?
164
- if obj.owning_group != obj.class.find(orig_val).owning_group
165
- raise DataError.new("Group-owned data cannot be transferred between groups")
166
- end
167
- end
168
-
169
- if self.class.offroad_group_data?
170
- if obj.class.offroad_group_data? && obj.owning_group && obj.owning_group.id != owning_group.id
171
- raise DataError.new("Invalid #{colname}: Group data cannot hold a foreign key to data owned by another group")
172
- end
173
- elsif self.class.offroad_global_data?
174
- unless obj.class.offroad_global_data?
175
- raise DataError.new("Invalid #{colname}: Global mirrored data cannot hold a foreign key to group data")
176
- end
177
- end
178
- end
179
- end
180
-
181
- end
182
-
183
- module GlobalDataInstanceMethods
184
- # Methods below this point are only to be used internally by Offroad
185
- # However, marking all of them private would make using them from elsewhere troublesome
186
-
187
- def locked_by_offroad?
188
- Offroad::app_offline?
189
- end
190
-
191
- #:nodoc#
192
- def before_mirrored_data_destroy
193
- raise ActiveRecord::ReadOnlyRecord if locked_by_offroad?
194
- return true
195
- end
196
-
197
- #:nodoc#
198
- def after_mirrored_data_destroy
199
- Offroad::SendableRecordState::note_record_destroyed(self) if Offroad::app_online?
200
- return true
201
- end
202
-
203
- #:nodoc#
204
- def before_mirrored_data_save
205
- raise ActiveRecord::ReadOnlyRecord if locked_by_offroad?
206
- validate_changed_id_columns
207
- return true
208
- end
209
-
210
- #:nodoc#
211
- def after_mirrored_data_save
212
- Offroad::SendableRecordState::note_record_created_or_updated(self) if Offroad::app_online? && changed?
213
- return true
214
- end
215
- end
216
-
217
- module GroupDataInstanceMethods
218
- def locked_by_offroad?
219
- return true if Offroad::app_online? && group_offline?
220
- return true if Offroad::app_offline? && (!group_state || group_state.group_locked?)
221
- return false
222
- end
223
-
224
- # If called on a group_owned_model, methods below bubble up to the group_base_model
225
-
226
- def offroad_group_lock!
227
- raise DataError.new("Cannot lock groups from online app") if Offroad::app_online?
228
- group_state.update_attribute(:group_locked, true)
229
- end
230
-
231
- # Returns a hash with the latest information about this group in the offline app
232
- def last_known_status
233
- raise DataError.new("This method is only for offline groups") if group_online?
234
- s = group_state
235
- fields_of_interest = [
236
- :last_installer_downloaded_at,
237
- :last_installation_at,
238
- :last_down_mirror_created_at,
239
- :last_down_mirror_loaded_at,
240
- :last_up_mirror_created_at,
241
- :last_up_mirror_loaded_at,
242
- :launcher_version,
243
- :app_version,
244
- :operating_system
245
- ]
246
- return fields_of_interest.map {|field_name| s.send(field_name)}
247
- end
248
-
249
- def group_offline?
250
- not group_online?
251
- end
252
-
253
- def group_online?
254
- return group_state.nil?
255
- end
256
-
257
- def group_offline=(b)
258
- raise DataError.new("Unable to change a group's offline status in offline app") if Offroad::app_offline?
259
-
260
- if b and Offroad::group_single_models.size > 0 and Offroad::GroupState.count > 0
261
- raise DataError.new("Unable to set more than one group offline if there are any group single models")
262
- end
263
-
264
- if b && !group_state
265
- Offroad::GroupState.for_group(owning_group).create!
266
- elsif group_state
267
- group_state.destroy
268
- end
269
- end
270
-
271
- def owning_group
272
- return nil if unlocked_group_single_record?
273
- return Offroad::GroupState.first.app_group if offroad_mode == :group_single
274
-
275
- # Recurse upwards until we get to the group base
276
- if self.class.offroad_group_base?
277
- return self
278
- else
279
- parent = send(offroad_parent_assoc.name)
280
- if parent
281
- return parent.owning_group
282
- else
283
- return nil
284
- end
285
- end
286
- end
287
-
288
- # Methods below this point are only to be used internally by Offroad
289
- # However, marking them private makes using them from elsewhere troublesome
290
-
291
- #:nodoc#
292
- def before_mirrored_data_destroy
293
- if group_offline? && offroad_mode == :group_base
294
- group_state.update_attribute(:group_being_destroyed, true)
295
- end
296
-
297
- return true if unlocked_group_single_record?
298
-
299
- if locked_by_offroad?
300
- # The only thing that can be deleted is the entire group (possibly with its dependent records), and only if we're online
301
- raise ActiveRecord::ReadOnlyRecord unless Offroad::app_online? and (offroad_mode == :group_base or group_being_destroyed)
302
- end
303
-
304
- # If the app is offline, the only thing that CAN'T be deleted even if unlocked is the group
305
- raise ActiveRecord::ReadOnlyRecord if Offroad::app_offline? and offroad_mode == :group_base
306
-
307
- return true
308
- end
309
-
310
- #:nodoc#
311
- def after_mirrored_data_destroy
312
- Offroad::SendableRecordState::note_record_destroyed(self) if Offroad::app_offline?
313
- Offroad::GroupState::note_group_destroyed(self) if group_offline? && offroad_mode == :group_base
314
- return true
315
- end
316
-
317
- #:nodoc#
318
- def before_mirrored_data_save
319
- return true if unlocked_group_single_record?
320
-
321
- raise DataError.new("Invalid owning group") unless owning_group
322
-
323
- if Offroad::app_offline?
324
- case offroad_mode
325
- when :group_base
326
- raise DataError.new("Cannot create groups in offline mode") if new_record?
327
- when :group_owned
328
- raise DataError.new("Owning group must be the offline group") if owning_group != Offroad::offline_group
329
- end
330
- end
331
-
332
- validate_changed_id_columns
333
-
334
- raise ActiveRecord::ReadOnlyRecord if locked_by_offroad?
335
-
336
- return true
337
- end
338
-
339
- #:nodoc#
340
- def after_mirrored_data_save
341
- Offroad::SendableRecordState::note_record_created_or_updated(self) if Offroad::app_offline? && changed?
342
- return true
343
- end
344
-
345
- #:nodoc#
346
- def group_state
347
- Offroad::GroupState.for_group(owning_group).first
348
- end
349
-
350
- #:nodoc:#
351
- def group_being_destroyed
352
- return true unless owning_group # If the group doesn't exist anymore, then it's pretty well destroyed
353
- return group_state.group_being_destroyed
354
- end
355
-
356
- #:nodoc:#
357
- def unlocked_group_single_record?
358
- offroad_mode == :group_single && Offroad::GroupState.count == 0
359
- end
360
- end
361
-
362
- module HoboPermissionsInstanceMethods
363
- private
364
-
365
- def pre_check_passed?(method_name)
366
- begin
367
- send(method_name)
368
- rescue ActiveRecord::ReadOnlyRecord
369
- return false
370
- rescue Offroad::DataError
371
- return false
372
- end
373
- return true
374
- end
375
- end
376
- end
377
- end
1
+ module Offroad
2
+ module ModelExtensions
3
+ OFFROAD_VALID_MODES = [:group_base, :group_owned, :group_single, :global]
4
+ OFFROAD_GROUP_MODES = [:group_base, :group_owned, :group_single]
5
+
6
+ def acts_as_offroadable(mode, opts = {})
7
+ raise ModelError.new("You can only call acts_as_offroadable once per model") if acts_as_offroadable?
8
+ raise ModelError.new("You must specify a mode, one of " + OFFROAD_VALID_MODES.map(&:inspect).join("/")) unless OFFROAD_VALID_MODES.include?(mode)
9
+
10
+ set_internal_cattr :offroad_mode, mode
11
+
12
+ case mode
13
+ when :group_owned then
14
+ raise ModelError.new("For :group_owned models, need to specify :parent") unless opts[:parent]
15
+ assoc = reflect_on_association(opts.delete(:parent))
16
+ raise ModelError.new("No such parent associaton") unless assoc
17
+ raise ModelError.new("Parent association must be a belongs_to association") unless assoc.belongs_to?
18
+ raise ModelError.new("Parent association must be to a group data model") unless assoc.klass.offroad_group_data?
19
+
20
+ set_internal_cattr :offroad_parent_assoc, assoc
21
+ Offroad::note_group_owned_model(self)
22
+ when :group_single then
23
+ Offroad::note_group_single_model(self)
24
+ when :group_base then
25
+ Offroad::note_group_base_model(self)
26
+ when :global then
27
+ Offroad::note_global_data_model(self)
28
+ end
29
+
30
+ # We should have deleted all the options from the hash by this point
31
+ raise ModelError.new("Unknown or inapplicable option(s) specified") unless opts.size == 0
32
+
33
+ case mode
34
+ when :group_base then
35
+ named_scope :owned_by_offroad_group, lambda { |group| { :conditions => { :id => group.id } } }
36
+ named_scope :offline_groups, {
37
+ :joins =>
38
+ "INNER JOIN \"#{Offroad::GroupState.table_name}\" ON \"#{Offroad::GroupState.table_name}\".app_group_id = \"#{table_name}\".\"#{primary_key}\""
39
+ }
40
+ named_scope :online_groups, {
41
+ :joins =>
42
+ "LEFT JOIN \"#{Offroad::GroupState.table_name}\" ON \"#{Offroad::GroupState.table_name}\".app_group_id = \"#{table_name}\".\"#{primary_key}\"",
43
+ :conditions =>
44
+ "\"#{Offroad::GroupState.table_name}\".app_group_id IS NULL"
45
+ }
46
+ when :group_owned then
47
+ named_scope :owned_by_offroad_group, lambda { |group| args_for_ownership_scope(group) }
48
+ when :group_single then
49
+ named_scope :owned_by_offroad_group, lambda { |group| {
50
+ :conditions => (Offroad::GroupState.count > 0 && group == Offroad::GroupState.first.app_group) ? "1=1" : "1=0"
51
+ } }
52
+ end
53
+
54
+ if offroad_group_data?
55
+ include GroupDataInstanceMethods
56
+ else
57
+ include GlobalDataInstanceMethods
58
+ end
59
+ include CommonInstanceMethods
60
+
61
+ before_destroy :before_mirrored_data_destroy
62
+ after_destroy :after_mirrored_data_destroy
63
+ before_save :before_mirrored_data_save
64
+ after_save :after_mirrored_data_save
65
+
66
+ if Object.const_defined?(:Hobo) and included_modules.include?(Hobo::Model)
67
+ include HoboPermissionsInstanceMethods
68
+ [
69
+ [:create, :save],
70
+ [:update, :save],
71
+ [:destroy, :destroy]
72
+ ].each do |perm_name, check_name|
73
+ define_method "#{perm_name}_permitted_with_offroad_check?".to_sym do
74
+ pre_check_passed?("before_mirrored_data_#{check_name}".to_sym) && send("#{perm_name}_permitted_without_offroad_check?")
75
+ end
76
+ alias_method_chain "#{perm_name}_permitted?", "offroad_check"
77
+ end
78
+ end
79
+ end
80
+
81
+ def offroad_model_state
82
+ model_scope = Offroad::ModelState::for_model(self)
83
+ return model_scope.first || model_scope.create
84
+ end
85
+
86
+ def acts_as_offroadable?
87
+ respond_to? :offroad_mode
88
+ end
89
+
90
+ def safe_to_load_from_cargo_stream?
91
+ acts_as_offroadable?
92
+ end
93
+
94
+ def offroad_group_base?
95
+ acts_as_offroadable? && offroad_mode == :group_base
96
+ end
97
+
98
+ def offroad_group_data?
99
+ acts_as_offroadable? && OFFROAD_GROUP_MODES.include?(offroad_mode)
100
+ end
101
+
102
+ def offroad_global_data?
103
+ acts_as_offroadable? && offroad_mode == :global
104
+ end
105
+
106
+ private
107
+
108
+ def set_internal_cattr(name, value)
109
+ write_inheritable_attribute name, value
110
+ class_inheritable_reader name
111
+ end
112
+
113
+ def args_for_ownership_scope(group)
114
+ included_assocs = []
115
+ conditions = []
116
+ assoc_owner = self
117
+ assoc = offroad_parent_assoc
118
+ while true
119
+ if assoc.klass.offroad_group_base?
120
+ conditions << "\"#{assoc_owner.table_name}\".\"#{assoc.primary_key_name}\" = #{group.id}"
121
+ break
122
+ else
123
+ conditions << "\"#{assoc_owner.table_name}\".\"#{assoc.primary_key_name}\" = \"#{assoc.klass.table_name}\".\"#{assoc.klass.primary_key}\""
124
+ included_assocs << assoc
125
+ assoc_owner = assoc.klass
126
+ assoc = assoc.klass.offroad_parent_assoc
127
+ end
128
+ end
129
+
130
+ includes = {}
131
+ included_assocs.reverse.each do |assoc|
132
+ includes = {assoc.name => includes}
133
+ end
134
+
135
+ return {:include => includes, :conditions => conditions.join(" AND ")}
136
+ end
137
+
138
+ module CommonInstanceMethods
139
+ # Methods below this point are only to be used internally by Offroad
140
+ # However, making all of them private would make using them from elsewhere troublesome
141
+
142
+ # TODO Should put common save and destroy wrappers in here, with access to a method that checks if SRS needed
143
+ # TODO That method should also be used in import_model_cargo instead of explicitly trying to find the srs
144
+
145
+ #:nodoc:#
146
+ def validate_changed_id_columns
147
+ changes.each do |colname, arr|
148
+ orig_val = arr[0]
149
+ new_val = arr[1]
150
+
151
+ raise DataError.new("Cannot change id of offroad-tracked records (orig #{orig_val.inspect}, new #{new_val.inspect}") if colname == self.class.primary_key && orig_val != nil
152
+
153
+ # FIXME : Use association reflection instead
154
+ next unless colname.end_with? "_id"
155
+ accessor_name = colname[0, colname.size-3]
156
+ next unless respond_to? accessor_name
157
+ obj = send(accessor_name)
158
+ next unless obj
159
+
160
+ raise DataError.new("Mirrored data cannot hold a foreign key to unmirrored data") unless obj.class.acts_as_offroadable?
161
+
162
+ if !new_record? and offroad_mode == :group_owned and colname == offroad_parent_assoc.primary_key_name
163
+ # obj is our parent
164
+ # FIXME: What if we can't find orig_val?
165
+ if obj.owning_group != obj.class.find(orig_val).owning_group
166
+ raise DataError.new("Group-owned data cannot be transferred between groups")
167
+ end
168
+ end
169
+
170
+ if self.class.offroad_group_data?
171
+ if obj.class.offroad_group_data? && obj.owning_group && obj.owning_group.id != owning_group.id
172
+ raise DataError.new("Invalid #{colname}: Group data cannot hold a foreign key to data owned by another group")
173
+ end
174
+ elsif self.class.offroad_global_data?
175
+ unless obj.class.offroad_global_data?
176
+ raise DataError.new("Invalid #{colname}: Global mirrored data cannot hold a foreign key to group data")
177
+ end
178
+ end
179
+ end
180
+ end
181
+
182
+ end
183
+
184
+ module GlobalDataInstanceMethods
185
+ # Methods below this point are only to be used internally by Offroad
186
+ # However, marking all of them private would make using them from elsewhere troublesome
187
+
188
+ def locked_by_offroad?
189
+ Offroad::app_offline?
190
+ end
191
+
192
+ #:nodoc#
193
+ def before_mirrored_data_destroy
194
+ raise ActiveRecord::ReadOnlyRecord if locked_by_offroad?
195
+ return true
196
+ end
197
+
198
+ #:nodoc#
199
+ def after_mirrored_data_destroy
200
+ Offroad::SendableRecordState::note_record_destroyed(self) if Offroad::app_online?
201
+ return true
202
+ end
203
+
204
+ #:nodoc#
205
+ def before_mirrored_data_save
206
+ raise ActiveRecord::ReadOnlyRecord if locked_by_offroad?
207
+ validate_changed_id_columns
208
+ return true
209
+ end
210
+
211
+ #:nodoc#
212
+ def after_mirrored_data_save
213
+ Offroad::SendableRecordState::note_record_created_or_updated(self) if Offroad::app_online? && changed?
214
+ return true
215
+ end
216
+ end
217
+
218
+ module GroupDataInstanceMethods
219
+ def locked_by_offroad?
220
+ return true if Offroad::app_online? && group_offline?
221
+ return true if Offroad::app_offline? && (!group_state || group_state.group_locked?)
222
+ return false
223
+ end
224
+
225
+ # If called on a group_owned_model, methods below bubble up to the group_base_model
226
+
227
+ def offroad_group_lock!
228
+ raise DataError.new("Cannot lock groups from online app") if Offroad::app_online?
229
+ group_state.update_attribute(:group_locked, true)
230
+ end
231
+
232
+ # Returns a hash with the latest information about this group in the offline app
233
+ def last_known_status
234
+ raise DataError.new("This method is only for offline groups") if group_online?
235
+ s = group_state
236
+ fields_of_interest = [
237
+ :last_installer_downloaded_at,
238
+ :last_installation_at,
239
+ :last_down_mirror_created_at,
240
+ :last_down_mirror_loaded_at,
241
+ :last_up_mirror_created_at,
242
+ :last_up_mirror_loaded_at,
243
+ :launcher_version,
244
+ :app_version,
245
+ :operating_system
246
+ ]
247
+ return fields_of_interest.map {|field_name| s.send(field_name)}
248
+ end
249
+
250
+ def group_offline?
251
+ not group_online?
252
+ end
253
+
254
+ def group_online?
255
+ return group_state.nil?
256
+ end
257
+
258
+ def group_offline=(b)
259
+ raise DataError.new("Unable to change a group's offline status in offline app") if Offroad::app_offline?
260
+
261
+ if b and Offroad::group_single_models.size > 0 and Offroad::GroupState.count > 0
262
+ raise DataError.new("Unable to set more than one group offline if there are any group single models")
263
+ end
264
+
265
+ if b && !group_state
266
+ Offroad::GroupState.for_group(owning_group).create!
267
+ elsif group_state
268
+ group_state.destroy
269
+ end
270
+ end
271
+
272
+ def owning_group
273
+ return nil if unlocked_group_single_record?
274
+ return Offroad::GroupState.first.app_group if offroad_mode == :group_single
275
+
276
+ # Recurse upwards until we get to the group base
277
+ if self.class.offroad_group_base?
278
+ return self
279
+ else
280
+ parent = send(offroad_parent_assoc.name)
281
+ if parent
282
+ return parent.owning_group
283
+ else
284
+ return nil
285
+ end
286
+ end
287
+ end
288
+
289
+ # Methods below this point are only to be used internally by Offroad
290
+ # However, marking them private makes using them from elsewhere troublesome
291
+
292
+ #:nodoc#
293
+ def before_mirrored_data_destroy
294
+ if group_offline? && offroad_mode == :group_base
295
+ group_state.update_attribute(:group_being_destroyed, true)
296
+ end
297
+
298
+ return true if unlocked_group_single_record?
299
+
300
+ if locked_by_offroad?
301
+ # The only thing that can be deleted is the entire group (possibly with its dependent records), and only if we're online
302
+ raise ActiveRecord::ReadOnlyRecord unless Offroad::app_online? and (offroad_mode == :group_base or group_being_destroyed)
303
+ end
304
+
305
+ # If the app is offline, the only thing that CAN'T be deleted even if unlocked is the group
306
+ raise ActiveRecord::ReadOnlyRecord if Offroad::app_offline? and offroad_mode == :group_base
307
+
308
+ return true
309
+ end
310
+
311
+ #:nodoc#
312
+ def after_mirrored_data_destroy
313
+ Offroad::SendableRecordState::note_record_destroyed(self) if Offroad::app_offline?
314
+ Offroad::GroupState::note_group_destroyed(self) if group_offline? && offroad_mode == :group_base
315
+ return true
316
+ end
317
+
318
+ #:nodoc#
319
+ def before_mirrored_data_save
320
+ return true if unlocked_group_single_record?
321
+
322
+ raise DataError.new("Invalid owning group") unless owning_group
323
+
324
+ if Offroad::app_offline?
325
+ case offroad_mode
326
+ when :group_base
327
+ raise DataError.new("Cannot create groups in offline mode") if new_record?
328
+ when :group_owned
329
+ raise DataError.new("Owning group must be the offline group") if owning_group != Offroad::offline_group
330
+ end
331
+ end
332
+
333
+ validate_changed_id_columns
334
+
335
+ raise ActiveRecord::ReadOnlyRecord if locked_by_offroad?
336
+
337
+ return true
338
+ end
339
+
340
+ #:nodoc#
341
+ def after_mirrored_data_save
342
+ Offroad::SendableRecordState::note_record_created_or_updated(self) if Offroad::app_offline? && changed?
343
+ return true
344
+ end
345
+
346
+ #:nodoc#
347
+ def group_state
348
+ Offroad::GroupState.for_group(owning_group).first
349
+ end
350
+
351
+ #:nodoc:#
352
+ def group_being_destroyed
353
+ return true unless owning_group # If the group doesn't exist anymore, then it's pretty well destroyed
354
+ return group_state.group_being_destroyed
355
+ end
356
+
357
+ #:nodoc:#
358
+ def unlocked_group_single_record?
359
+ offroad_mode == :group_single && Offroad::GroupState.count == 0
360
+ end
361
+ end
362
+
363
+ module HoboPermissionsInstanceMethods
364
+ private
365
+
366
+ def pre_check_passed?(method_name)
367
+ begin
368
+ send(method_name)
369
+ rescue ActiveRecord::ReadOnlyRecord
370
+ return false
371
+ rescue Offroad::DataError
372
+ return false
373
+ end
374
+ return true
375
+ end
376
+ end
377
+ end
378
+ end