offroad 0.0.2 → 0.0.3

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 (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