kojac 0.9.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.
- checksums.yaml +15 -0
- data/.gitignore +7 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +158 -0
- data/MIT-LICENSE +20 -0
- data/README.md +69 -0
- data/Rakefile +27 -0
- data/app/assets/javascripts/can_extensions.js +45 -0
- data/app/assets/javascripts/kojac.js +1230 -0
- data/app/assets/javascripts/kojac_canjs.js +191 -0
- data/app/assets/javascripts/kojac_ember.js +463 -0
- data/app/controllers/kojac_controller.rb +70 -0
- data/app/serializers/default_kojac_serializer.rb +10 -0
- data/diagram.odg +0 -0
- data/kojac.gemspec +36 -0
- data/lib/kojac/app_serialize.rb +29 -0
- data/lib/kojac/kojac_rails.rb +432 -0
- data/lib/kojac/ring_strong_parameters.rb +195 -0
- data/lib/kojac/version.rb +3 -0
- data/lib/kojac.rb +8 -0
- data/lib/tasks/kojac_tasks.rake +4 -0
- data/notes.txt +48 -0
- data/spec/.DS_Store +0 -0
- data/spec/can_cache_spec.js +87 -0
- data/spec/can_factory_spec.js +144 -0
- data/spec/can_model_spec.js +127 -0
- data/spec/demo/README.rdoc +261 -0
- data/spec/demo/Rakefile +7 -0
- data/spec/demo/app/assets/javascripts/application.js +15 -0
- data/spec/demo/app/assets/stylesheets/application.css +13 -0
- data/spec/demo/app/controllers/application_controller.rb +3 -0
- data/spec/demo/app/helpers/application_helper.rb +2 -0
- data/spec/demo/app/mailers/.gitkeep +0 -0
- data/spec/demo/app/models/.gitkeep +0 -0
- data/spec/demo/app/views/layouts/application.html.erb +14 -0
- data/spec/demo/config/application.rb +65 -0
- data/spec/demo/config/boot.rb +10 -0
- data/spec/demo/config/database.yml +25 -0
- data/spec/demo/config/environment.rb +5 -0
- data/spec/demo/config/environments/development.rb +37 -0
- data/spec/demo/config/environments/production.rb +67 -0
- data/spec/demo/config/environments/test.rb +37 -0
- data/spec/demo/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/demo/config/initializers/inflections.rb +15 -0
- data/spec/demo/config/initializers/mime_types.rb +5 -0
- data/spec/demo/config/initializers/secret_token.rb +7 -0
- data/spec/demo/config/initializers/session_store.rb +8 -0
- data/spec/demo/config/initializers/wrap_parameters.rb +14 -0
- data/spec/demo/config/locales/en.yml +5 -0
- data/spec/demo/config/routes.rb +58 -0
- data/spec/demo/config.ru +4 -0
- data/spec/demo/lib/assets/.gitkeep +0 -0
- data/spec/demo/log/.gitkeep +0 -0
- data/spec/demo/public/404.html +26 -0
- data/spec/demo/public/422.html +26 -0
- data/spec/demo/public/500.html +25 -0
- data/spec/demo/public/favicon.ico +0 -0
- data/spec/demo/script/rails +6 -0
- data/spec/ember_factory_spec.js +157 -0
- data/spec/ember_model_spec.js +179 -0
- data/spec/external/.DS_Store +0 -0
- data/spec/external/ember/.DS_Store +0 -0
- data/spec/external/ember/ember-1.0.0-rc.6.js +30970 -0
- data/spec/external/ember/handlebars-1.0.0-rc.4.js +2239 -0
- data/spec/external/jasmine/MIT.LICENSE +20 -0
- data/spec/external/jasmine/jasmine-html.js +616 -0
- data/spec/external/jasmine/jasmine.css +81 -0
- data/spec/external/jasmine/jasmine.js +2529 -0
- data/spec/external/jasmine.async.js +51 -0
- data/spec/external/jquery/jquery-1.7.2.js +9404 -0
- data/spec/external/jquery/jquery-1.7.2.min.js +4 -0
- data/spec/external/jquery/jquery-1.9.1.js +9597 -0
- data/spec/external/json2.js +480 -0
- data/spec/external/steal/steal-121115.js +2747 -0
- data/spec/external/steal/steal-3.2.3.js +2098 -0
- data/spec/external/steal/steal-3.2.3.min.js +27 -0
- data/spec/external/steal/steal.js +2466 -0
- data/spec/external/steal/steal.min.js +32 -0
- data/spec/external/steal/stealconfig.js +19 -0
- data/spec/external/underscore.js +1223 -0
- data/spec/external/underscore_plus.js +261 -0
- data/spec/external.zip +0 -0
- data/spec/handler_stack_spec.js +143 -0
- data/spec/helpers/SpecHelper.js +10 -0
- data/spec/kojac_caching_spec.js +105 -0
- data/spec/kojac_mock_spec.js +230 -0
- data/spec/kojac_model_spec.js +126 -0
- data/spec/kojac_object_spec.js +171 -0
- data/spec/kojac_operations_spec.js +41 -0
- data/spec/mockjson/order_item.js +37 -0
- data/spec/mockjson/order_item__49.js +15 -0
- data/spec/mockjson/order_item__50.js +15 -0
- data/spec/mockjson/order_item__51.js +15 -0
- data/spec/mockjson/product.js +82 -0
- data/spec/mockjson/product__3.js +22 -0
- data/spec/model_ring_spec.rb +52 -0
- data/spec/operation_include_spec.js +77 -0
- data/spec/run.html +81 -0
- data/spec/spec.js +2 -0
- data/spec/support/jasmine.yml +86 -0
- metadata +380 -0
data/diagram.odg
ADDED
|
Binary file
|
data/kojac.gemspec
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
|
2
|
+
|
|
3
|
+
# Maintain your gem's version:
|
|
4
|
+
require "kojac/version"
|
|
5
|
+
|
|
6
|
+
# Describe your gem and declare its dependencies:
|
|
7
|
+
Gem::Specification.new do |s|
|
|
8
|
+
s.name = "kojac"
|
|
9
|
+
s.version = Kojac::VERSION
|
|
10
|
+
s.authors = ["Gary McGhee"]
|
|
11
|
+
s.email = ["contact@buzzware.com.au"]
|
|
12
|
+
s.homepage = "https://github.com/buzzware/KOJAC"
|
|
13
|
+
s.license = "MIT"
|
|
14
|
+
|
|
15
|
+
s.summary = "KOJAC is an opinionated design and implementation for data management within Single Page Applications."
|
|
16
|
+
s.description = "KOJAC is an opinionated design and implementation for data management within Single Page Applications. It relates most heavily to the client and data protocol. The server may continue the key/value style down to a key/value-style database if desired, but that is not necessary. KOJAC also supports standard REST-style servers."
|
|
17
|
+
|
|
18
|
+
#s.files = Dir["{app,config,db,lib}/**/*"] + ["MIT-LICENSE", "Rakefile", "README.rdoc"]
|
|
19
|
+
s.files = `git ls-files`.split($/)
|
|
20
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
21
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
|
22
|
+
s.require_paths = ["lib"]
|
|
23
|
+
|
|
24
|
+
s.add_dependency "underscore_plus"
|
|
25
|
+
s.add_dependency "json2-rails"
|
|
26
|
+
s.add_dependency "jquery-rails"
|
|
27
|
+
s.add_dependency 'strong_parameters'
|
|
28
|
+
|
|
29
|
+
s.add_development_dependency "rails", "~> 3.2"
|
|
30
|
+
s.add_development_dependency "rspec-rails"
|
|
31
|
+
s.add_development_dependency "canjs-rails"
|
|
32
|
+
s.add_development_dependency "ember-rails"
|
|
33
|
+
s.add_development_dependency "jquery-rails"
|
|
34
|
+
s.add_development_dependency "capybara"
|
|
35
|
+
s.add_development_dependency "sqlite3"
|
|
36
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'active_record/serializer_override'
|
|
2
|
+
|
|
3
|
+
def app_serialize(aObject,aScope)
|
|
4
|
+
result = case aObject.class
|
|
5
|
+
when Fixnum then aObject.to_s
|
|
6
|
+
when Bignum then aObject.to_s
|
|
7
|
+
when Array
|
|
8
|
+
sz_class = ActiveModel::ArraySerializer
|
|
9
|
+
sz_class.new(aObject).to_json(:scope => aScope, :root => false)
|
|
10
|
+
when String then aObject
|
|
11
|
+
when FalseClass then 'false'
|
|
12
|
+
when TrueClass then 'true'
|
|
13
|
+
when Symbol then aObject.to_s
|
|
14
|
+
#when Hash
|
|
15
|
+
#sz_class = ActiveModel::Serializer
|
|
16
|
+
#sz_class.new(aObject).to_json(:scope => aScope, :root => false)
|
|
17
|
+
|
|
18
|
+
else
|
|
19
|
+
sz_class = aObject.respond_to?(:active_model_serializer) && aObject.send(:active_model_serializer)
|
|
20
|
+
sz_class = DefaultKojacSerializer if !sz_class && aObject.is_a?(ActiveModel)
|
|
21
|
+
if sz_class
|
|
22
|
+
sz_class.new(aObject).to_json(:scope => aScope, :root => false)
|
|
23
|
+
else
|
|
24
|
+
aObject.to_json
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
end
|
|
28
|
+
result
|
|
29
|
+
end
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
#require File.expand_path('ring_strong_parameters',File.dirname(__FILE__))
|
|
2
|
+
|
|
3
|
+
Kernel.class_eval do
|
|
4
|
+
def key_join(aResource,aId=nil,aAssoc=nil)
|
|
5
|
+
result = aResource
|
|
6
|
+
if aId
|
|
7
|
+
result += "__#{aId}"
|
|
8
|
+
result += ".#{aAssoc}" if aAssoc
|
|
9
|
+
end
|
|
10
|
+
result
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class String
|
|
15
|
+
def is_i?
|
|
16
|
+
!!(self =~ /^[-+]?[0-9]+$/)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def split_kojac_key
|
|
20
|
+
r,ia = self.split('__')
|
|
21
|
+
id,a = ia.split('.') if ia
|
|
22
|
+
id = id.to_i if id && id.is_i?
|
|
23
|
+
[r,id,a]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# eg deals__5 => deals
|
|
27
|
+
def resource
|
|
28
|
+
self.split('__')[0]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# eg. deals__5.options => deals__5
|
|
32
|
+
def base_key
|
|
33
|
+
self.split('.')[0]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def key_assoc
|
|
37
|
+
self.split('.')[1]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
module KojacUtils
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
def model_class_for_key(aKey)
|
|
45
|
+
resource = aKey.split_kojac_key[0]
|
|
46
|
+
resource.singularize.camelize.constantize rescue nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def model_for_key(aKey)
|
|
50
|
+
klass = KojacUtils.model_class_for_key(aKey)
|
|
51
|
+
resource,id,assoc = aKey.split_kojac_key
|
|
52
|
+
klass.find(id) rescue nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def upgrade_hashes_to_params(aValue)
|
|
56
|
+
if aValue.is_a? Hash
|
|
57
|
+
aValue = ActionController::Parameters.new(aValue) unless aValue.is_a?(ActionController::Parameters)
|
|
58
|
+
elsif aValue.is_a? Array
|
|
59
|
+
aValue = aValue.map {|v| upgrade_hashes_to_params(v)}
|
|
60
|
+
end
|
|
61
|
+
aValue
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
module Kojac::ModelMethods
|
|
67
|
+
|
|
68
|
+
def self.included(aClass)
|
|
69
|
+
aClass.send :extend, ClassMethods
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
module ClassMethods
|
|
73
|
+
def by_key(aKey,aContext=nil)
|
|
74
|
+
r,id,a = aKey.split_kojac_key
|
|
75
|
+
model = self
|
|
76
|
+
model = self.rescope(model,aContext) if self.respond_to? :rescope
|
|
77
|
+
if id
|
|
78
|
+
model.where(id: id).first
|
|
79
|
+
else
|
|
80
|
+
model.all
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def kojac_key
|
|
86
|
+
self.class.to_s.snake_case.pluralize+'__'+self.id.to_s
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def update_permitted_attributes!(aChanges, aRing)
|
|
90
|
+
aChanges = KojacUtils.upgrade_hashes_to_params(aChanges)
|
|
91
|
+
permitted_fields = self.class.permitted_fields(:write, aRing)
|
|
92
|
+
permitted_fields = aChanges.permit(*permitted_fields)
|
|
93
|
+
assign_attributes(permitted_fields, :without_protection => true)
|
|
94
|
+
save!
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
module Kojac::ControllerOpMethods
|
|
100
|
+
|
|
101
|
+
def self.included(aClass)
|
|
102
|
+
#aClass.send :extend, ClassMethods
|
|
103
|
+
aClass.send :include, ActiveSupport::Callbacks
|
|
104
|
+
aClass.send :define_callbacks, :update_op, :scope => [:kind, :name]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
#module ClassMethods
|
|
108
|
+
#end
|
|
109
|
+
|
|
110
|
+
module_function
|
|
111
|
+
|
|
112
|
+
public
|
|
113
|
+
|
|
114
|
+
attr_accessor :item
|
|
115
|
+
|
|
116
|
+
def results
|
|
117
|
+
@results ||= {}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def deduce_model_class
|
|
121
|
+
KojacUtils.model_class_for_key(self.kojac_resource)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def kojac_resource
|
|
125
|
+
self.class.to_s.chomp('Controller').snake_case
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def create_on_association(aItem,aAssoc,aValues,aRing)
|
|
129
|
+
raise "User does not have permission for create on #{aAssoc}" unless aItem.class.permitted_associations(:create,aRing).include?(aAssoc.to_sym)
|
|
130
|
+
|
|
131
|
+
return nil unless ma = aItem.class.reflect_on_association(aAssoc.to_sym)
|
|
132
|
+
a_model_class = ma.klass
|
|
133
|
+
|
|
134
|
+
aValues = KojacUtils.upgrade_hashes_to_params(aValues || {})
|
|
135
|
+
|
|
136
|
+
case ma.macro
|
|
137
|
+
when :belongs_to
|
|
138
|
+
return nil if !aValues.is_a?(Hash)
|
|
139
|
+
fields = aValues.permit( *a_model_class.permitted_fields(:write,aRing) )
|
|
140
|
+
return aItem.send("build_#{aAssoc}".to_sym,fields)
|
|
141
|
+
when :has_many
|
|
142
|
+
aValues = [aValues] if aValues.is_a?(Hash)
|
|
143
|
+
return nil unless aValues.is_a? Array
|
|
144
|
+
aValues.each do |v|
|
|
145
|
+
fields = v.permit( *a_model_class.permitted_fields(:write,aRing) )
|
|
146
|
+
new_sub_item = nil
|
|
147
|
+
case ma.macro
|
|
148
|
+
when :has_many
|
|
149
|
+
new_sub_item = aItem.send(aAssoc.to_sym).create(fields)
|
|
150
|
+
else
|
|
151
|
+
raise "#{ma.macro} association unsupported in CREATE"
|
|
152
|
+
end
|
|
153
|
+
merge_model_into_results(new_sub_item)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
#
|
|
157
|
+
#
|
|
158
|
+
#
|
|
159
|
+
#a_value = op[:value][a] # get data for this association, assume {}
|
|
160
|
+
#if a_value.is_a?(Hash)
|
|
161
|
+
# a_model_class = ma.klass
|
|
162
|
+
# fields = a_value.permit( *permitted_fields(:write,a_model_class) )
|
|
163
|
+
# item.send("build_#{a}".to_sym,fields)
|
|
164
|
+
# included_assocs << a.to_sym
|
|
165
|
+
#elsif a_value.is_a?(Array)
|
|
166
|
+
# raise "association collections not yet implemented for create"
|
|
167
|
+
#else
|
|
168
|
+
# next
|
|
169
|
+
#end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def create_op
|
|
173
|
+
ring = current_user.try(:ring)
|
|
174
|
+
op = params[:op]
|
|
175
|
+
options = op[:options] || {}
|
|
176
|
+
model_class = deduce_model_class
|
|
177
|
+
resource,id,assoc = op['key'].split_kojac_key
|
|
178
|
+
if assoc # create operation on an association eg. {verb: "CREATE", key: "order.items"}
|
|
179
|
+
raise "User does not have permission for #{op[:verb]} operation on #{model_class.to_s}.#{assoc}" unless model_class.permitted_associations(:create,ring).include?(assoc.to_sym)
|
|
180
|
+
item = KojacUtils.model_for_key(key_join(resource,id))
|
|
181
|
+
ma = model_class.reflect_on_association(assoc.to_sym)
|
|
182
|
+
a_value = op[:value] # get data for this association, assume {}
|
|
183
|
+
raise "create multiple not yet implemented for associations" unless a_value.is_a?(Hash)
|
|
184
|
+
|
|
185
|
+
a_model_class = ma.klass
|
|
186
|
+
p_fields = a_model_class.permitted_fields(:write,ring)
|
|
187
|
+
fields = a_value.permit( *p_fields )
|
|
188
|
+
new_sub_item = nil
|
|
189
|
+
case ma.macro
|
|
190
|
+
when :has_many
|
|
191
|
+
new_sub_item = item.send(assoc.to_sym).create(fields)
|
|
192
|
+
else
|
|
193
|
+
raise "#{ma.macro} association unsupported in CREATE"
|
|
194
|
+
end
|
|
195
|
+
result_key = op[:result_key] || new_sub_item.kojac_key
|
|
196
|
+
merge_model_into_results(new_sub_item)
|
|
197
|
+
else # create operation on a resource eg. {verb: "CREATE", key: "order_items"} but may have embedded association values
|
|
198
|
+
p_fields = model_class.permitted_fields(:write,ring)
|
|
199
|
+
raise "User does not have permission for #{op[:verb]} operation on #{model_class.to_s}" unless model_class.ring_can?(:create,ring)
|
|
200
|
+
|
|
201
|
+
p_fields = op[:value].permit( *p_fields )
|
|
202
|
+
item = model_class.create!(p_fields)
|
|
203
|
+
|
|
204
|
+
options_include = options['include'] || []
|
|
205
|
+
included_assocs = []
|
|
206
|
+
p_assocs = model_class.permitted_associations(:write,ring)
|
|
207
|
+
if p_assocs
|
|
208
|
+
p_assocs.each do |a|
|
|
209
|
+
next unless (a_value = op[:value][a]) || options_include.include?(a.to_s)
|
|
210
|
+
create_on_association(item,a,a_value,ring)
|
|
211
|
+
included_assocs << a.to_sym
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
item.save!
|
|
215
|
+
result_key = op[:result_key] || item.kojac_key
|
|
216
|
+
merge_model_into_results(item,result_key,:include => included_assocs)
|
|
217
|
+
end
|
|
218
|
+
{
|
|
219
|
+
key: op[:key],
|
|
220
|
+
verb: op[:verb],
|
|
221
|
+
result_key: result_key,
|
|
222
|
+
results: results
|
|
223
|
+
}
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
protected
|
|
227
|
+
|
|
228
|
+
def merge_model_into_results(aItem,aResultKey=nil,aOptions=nil)
|
|
229
|
+
ring = current_user.try(:ring)
|
|
230
|
+
aResultKey ||= aItem.kojac_key
|
|
231
|
+
aOptions ||= {}
|
|
232
|
+
results[aResultKey] = aItem.sanitized_hash(ring)
|
|
233
|
+
if included_assocs = aOptions[:include]
|
|
234
|
+
included_assocs = included_assocs.split(',') if included_assocs.is_a?(String)
|
|
235
|
+
included_assocs = [included_assocs] unless included_assocs.is_a?(Array)
|
|
236
|
+
included_assocs.map!(&:to_sym) if included_assocs.is_a?(Array)
|
|
237
|
+
p_assocs = aItem.class.permitted_associations(:read,ring)
|
|
238
|
+
use_assocs = p_assocs.delete_if do |a|
|
|
239
|
+
if included_assocs.include?(a) and ma = aItem.class.reflect_on_association(a)
|
|
240
|
+
![:belongs_to,:has_many].include?(ma.macro) # is supported association type
|
|
241
|
+
else
|
|
242
|
+
true # no such assoc
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
use_assocs.each do |a|
|
|
246
|
+
next unless a_contents = aItem.send(a)
|
|
247
|
+
if a_contents.is_a? Array
|
|
248
|
+
contents_h = []
|
|
249
|
+
a_contents.each do |sub_item|
|
|
250
|
+
results[sub_item.kojac_key] = sub_item.sanitized_hash(ring)
|
|
251
|
+
#contents_h << sub_item.id
|
|
252
|
+
end
|
|
253
|
+
#results[aResultKey] = contents_h
|
|
254
|
+
else
|
|
255
|
+
results[a_contents.kojac_key] = a_contents.sanitized_hash(ring)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
public
|
|
262
|
+
|
|
263
|
+
def read_op
|
|
264
|
+
op = params[:op]
|
|
265
|
+
key = op[:key]
|
|
266
|
+
result_key = nil
|
|
267
|
+
resource,id = key.split '__'
|
|
268
|
+
model = deduce_model_class
|
|
269
|
+
|
|
270
|
+
if id # item
|
|
271
|
+
if model
|
|
272
|
+
item = model.by_key(key,op)
|
|
273
|
+
result_key = op[:result_key] || (item && item.kojac_key) || op[:key]
|
|
274
|
+
merge_model_into_results(item,result_key,op[:options])
|
|
275
|
+
else
|
|
276
|
+
result_key = op[:result_key] || op[:key]
|
|
277
|
+
results[result_key] = null
|
|
278
|
+
end
|
|
279
|
+
else # collection
|
|
280
|
+
result_key = op[:result_key] || op[:key]
|
|
281
|
+
results[result_key] = []
|
|
282
|
+
if model
|
|
283
|
+
items = model.by_key(key,op)
|
|
284
|
+
items.each do |m|
|
|
285
|
+
item_key = m.kojac_key
|
|
286
|
+
results[result_key] << item_key.bite(resource+'__')
|
|
287
|
+
merge_model_into_results(m,item_key,op[:options])
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
{
|
|
292
|
+
key: op[:key],
|
|
293
|
+
verb: op[:verb],
|
|
294
|
+
results: results,
|
|
295
|
+
result_key: result_key
|
|
296
|
+
}
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def update_op
|
|
300
|
+
result = nil
|
|
301
|
+
model = deduce_model_class
|
|
302
|
+
|
|
303
|
+
ring = current_user.try(:ring)
|
|
304
|
+
op = params[:op]
|
|
305
|
+
result_key = nil
|
|
306
|
+
if self.item = model.by_key(op[:key],op)
|
|
307
|
+
|
|
308
|
+
run_callbacks :update_op do
|
|
309
|
+
item.update_permitted_attributes!(op[:value], ring)
|
|
310
|
+
|
|
311
|
+
associations = model.permitted_associations(:write,ring)
|
|
312
|
+
associations.each do |k|
|
|
313
|
+
next unless assoc = model.reflect_on_association(k)
|
|
314
|
+
next unless op[:value][k]
|
|
315
|
+
case assoc.macro
|
|
316
|
+
when :belongs_to
|
|
317
|
+
if leaf = (item.send(k) || item.send("build_#{k}".to_sym))
|
|
318
|
+
#permitted_fields = leaf.class.permitted_fields(:write,ring)
|
|
319
|
+
#permitted_fields = op[:value][k].permit( *permitted_fields )
|
|
320
|
+
#leaf.assign_attributes(permitted_fields, :without_protection => true)
|
|
321
|
+
#leaf.save!
|
|
322
|
+
leaf.update_permitted_attributes!(op[:value][k], ring)
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
result_key = item.kojac_key
|
|
328
|
+
results[result_key] = item
|
|
329
|
+
|
|
330
|
+
associations.each do |a|
|
|
331
|
+
next unless assoc_item = item.send(a)
|
|
332
|
+
next unless key = assoc_item.respond_to?(:kojac_key) && assoc_item.kojac_key
|
|
333
|
+
results[key] = assoc_item
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
{
|
|
338
|
+
key: op[:key],
|
|
339
|
+
verb: op[:verb],
|
|
340
|
+
result_key: result_key,
|
|
341
|
+
results: results
|
|
342
|
+
}
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def destroy_op
|
|
346
|
+
ring = current_user.try(:ring)
|
|
347
|
+
op = params[:op]
|
|
348
|
+
result_key = op[:result_key] || op[:key]
|
|
349
|
+
item = KojacUtils.model_for_key(op[:key])
|
|
350
|
+
item.destroy if item
|
|
351
|
+
results[result_key] = nil
|
|
352
|
+
{
|
|
353
|
+
key: op[:key],
|
|
354
|
+
verb: op[:verb],
|
|
355
|
+
result_key: result_key,
|
|
356
|
+
results: results
|
|
357
|
+
}
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
#def execute_op
|
|
361
|
+
# puts 'execute_op'
|
|
362
|
+
#end
|
|
363
|
+
|
|
364
|
+
def add_op
|
|
365
|
+
ring = current_user.try(:ring)
|
|
366
|
+
op = params[:op]
|
|
367
|
+
model = deduce_model_class
|
|
368
|
+
raise "ADD only supports associated collections at present eg order.items" unless op[:key].index('.')
|
|
369
|
+
|
|
370
|
+
item = KojacUtils.model_for_key(op[:key].base_key)
|
|
371
|
+
assoc = (assoc=op[:key].key_assoc) && assoc.to_sym
|
|
372
|
+
id = op[:value]['id']
|
|
373
|
+
|
|
374
|
+
ma = item.class.reflect_on_association(assoc)
|
|
375
|
+
case ma.macro
|
|
376
|
+
when :has_many
|
|
377
|
+
assoc_class = ma.klass
|
|
378
|
+
assoc_item = assoc_class.find(id)
|
|
379
|
+
item.send(assoc) << assoc_item
|
|
380
|
+
|
|
381
|
+
#ids_method = assoc.to_s.singularize+'_ids'
|
|
382
|
+
#ids = item.send(ids_method.to_sym)
|
|
383
|
+
#item.send((ids_method+'=').to_sym,ids + [id])
|
|
384
|
+
result_key = assoc_item.kojac_key
|
|
385
|
+
merge_model_into_results(assoc_item)
|
|
386
|
+
else
|
|
387
|
+
raise "ADD does not yet support #{ma.macro} associations"
|
|
388
|
+
end
|
|
389
|
+
{
|
|
390
|
+
key: op[:key],
|
|
391
|
+
verb: op[:verb],
|
|
392
|
+
result_key: result_key,
|
|
393
|
+
results: results
|
|
394
|
+
}
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def remove_op
|
|
398
|
+
ring = current_user.try(:ring)
|
|
399
|
+
op = params[:op]
|
|
400
|
+
model = deduce_model_class
|
|
401
|
+
raise "REMOVE only supports associated collections at present eg order.items" unless op[:key].key_assoc
|
|
402
|
+
|
|
403
|
+
item = KojacUtils.model_for_key(op[:key].base_key)
|
|
404
|
+
assoc = (assoc=op[:key].key_assoc) && assoc.to_sym
|
|
405
|
+
id = op[:value]['id']
|
|
406
|
+
|
|
407
|
+
ma = item.class.reflect_on_association(assoc)
|
|
408
|
+
case ma.macro
|
|
409
|
+
when :has_many
|
|
410
|
+
assoc_class = ma.klass
|
|
411
|
+
if assoc_item = item.send(assoc).find(id)
|
|
412
|
+
item.send(assoc).delete(assoc_item)
|
|
413
|
+
result_key = assoc_item.kojac_key
|
|
414
|
+
if (assoc_item.destroyed?)
|
|
415
|
+
results[result_key] = nil
|
|
416
|
+
else
|
|
417
|
+
merge_model_into_results(assoc_item,result_key)
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
else
|
|
421
|
+
raise "REMOVE does not yet support #{ma.macro} associations"
|
|
422
|
+
end
|
|
423
|
+
{
|
|
424
|
+
key: op[:key],
|
|
425
|
+
verb: op[:verb],
|
|
426
|
+
result_key: result_key,
|
|
427
|
+
results: results
|
|
428
|
+
}
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
end
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#ring_strong_parameters
|
|
2
|
+
#
|
|
3
|
+
#Assists implementation of ring level security (http://en.wikipedia.org/wiki/Ring_(computer_security)) with Rails 4 (or Rails 3 with gem) Strong Parameters.
|
|
4
|
+
#
|
|
5
|
+
#Ring Level Security is a simpler alternative to Role Based Security. Rings are arranged in a concentric hierarchy from most-privileged innermost Ring 0 to the least privileged highest ring number. Users have their own ring level which gives them access to that ring and below.
|
|
6
|
+
#
|
|
7
|
+
#For example, a sysadmin could have Ring 0, a website manager ring 1, a customer ring 2, and anonymous users ring 3. A customer would have all the capabilities of anonymous users, and more. Likewise, a website manager has all the capabilities of a customer, and more etc.
|
|
8
|
+
#
|
|
9
|
+
#This inheritance of capabilities of outer rings, and the simple assigning of users to rings, makes security rules less repetitive and easier to write and maintain, minimising dangerous mistakes.
|
|
10
|
+
#
|
|
11
|
+
#This gem does not affect or replace or prevent the standard strong parameters methods from being used in parallel, it merely generates arguments for the standard strong parameters methods.
|
|
12
|
+
#
|
|
13
|
+
#
|
|
14
|
+
#
|
|
15
|
+
#BASIC_FIELDS = [:name, :address]
|
|
16
|
+
#
|
|
17
|
+
#class Deal
|
|
18
|
+
# ring 1, :write, BASIC_FIELDS
|
|
19
|
+
# ring 1, :write, :phone
|
|
20
|
+
# ring 1, :delete
|
|
21
|
+
# ring 2, :read, BASIC_FIELDS
|
|
22
|
+
#end
|
|
23
|
+
#
|
|
24
|
+
#
|
|
25
|
+
#class DealsController
|
|
26
|
+
#
|
|
27
|
+
# def update
|
|
28
|
+
# ring_fields(:write,model)
|
|
29
|
+
# if ring_can(:write,model,:name)
|
|
30
|
+
# if ring_can(:delete,model)
|
|
31
|
+
# model.update(params.permit( ring_fields(:write,model) ))
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
#end
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RingStrongParameters
|
|
39
|
+
|
|
40
|
+
cattr_accessor :config
|
|
41
|
+
|
|
42
|
+
def self.lookup_ring(aRingName)
|
|
43
|
+
return nil if !aRingName
|
|
44
|
+
return aRingName if aRingName.is_a?(Fixnum)
|
|
45
|
+
if ring_names = RingStrongParameters.config[:ring_names]
|
|
46
|
+
return ring_names[aRingName.to_sym]
|
|
47
|
+
else
|
|
48
|
+
return nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# see http://yehudakatz.com/2009/11/12/better-ruby-idioms/ re class and instance methods and modules
|
|
56
|
+
|
|
57
|
+
module RingStrongParameters::Model
|
|
58
|
+
|
|
59
|
+
def self.included(aClass)
|
|
60
|
+
aClass.cattr_accessor :rings_fields
|
|
61
|
+
aClass.rings_fields = [] # [1] => {read: [:name,:address], delete: true}
|
|
62
|
+
aClass.cattr_accessor :rings_abilities
|
|
63
|
+
aClass.rings_abilities = [] # [1] => {read: [:name,:address], delete: true}
|
|
64
|
+
aClass.send :extend, ClassMethods
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def sanitized_hash(aRing)
|
|
68
|
+
p_fields = self.class.permitted_fields(:read, aRing)
|
|
69
|
+
self.attributes.filter_include(p_fields)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
module ClassMethods
|
|
73
|
+
|
|
74
|
+
# supports different formats :
|
|
75
|
+
# ring :sales, :write => [:name,:address] ie. sales can write the name and address fields
|
|
76
|
+
# ring :sales, :read ie. sales can read this model
|
|
77
|
+
# ring :sales, [:read, :create, :destroy] ie. sales can read, create and destroy this model
|
|
78
|
+
def ring(aRing,aAbilities)
|
|
79
|
+
aRing = RingStrongParameters.lookup_ring(aRing)
|
|
80
|
+
raise "aRing must be a number or a symbol defined in RingStrongParameters.config.ring_names" if !aRing.is_a?(Fixnum)
|
|
81
|
+
|
|
82
|
+
if aAbilities.is_a? Hash # eg. fields like :write => [:name,:address]
|
|
83
|
+
ring_rec = self.rings_fields[aRing] || {}
|
|
84
|
+
aAbilities.each do |abilities,fields|
|
|
85
|
+
abilities = [abilities] unless abilities.is_a?(Array)
|
|
86
|
+
fields = [fields] unless fields.is_a?(Array)
|
|
87
|
+
abilities.each do |a|
|
|
88
|
+
a = a.to_sym
|
|
89
|
+
ring_fields = ring_rec[a] || []
|
|
90
|
+
ring_fields = ring_fields + fields.map(&:to_sym)
|
|
91
|
+
ring_fields.uniq!
|
|
92
|
+
ring_fields.sort!
|
|
93
|
+
ring_rec[a] = ring_fields
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
self.rings_fields[aRing] = ring_rec
|
|
97
|
+
elsif aAbilities.is_a?(Array) || aAbilities.is_a?(Symbol) # eg. abilities like :sales, [:read, :create, :destroy]
|
|
98
|
+
aAbilities = [aAbilities] unless aAbilities.is_a?(Array)
|
|
99
|
+
ring_ab = self.rings_abilities[aRing] || []
|
|
100
|
+
aAbilities.each do |ability|
|
|
101
|
+
ring_ab << ability.to_sym
|
|
102
|
+
end
|
|
103
|
+
ring_ab.uniq!
|
|
104
|
+
ring_ab.sort!
|
|
105
|
+
self.rings_abilities[aRing] = ring_ab
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def permitted(aAbility,aRing)
|
|
110
|
+
aRing = RingStrongParameters.lookup_ring(aRing)
|
|
111
|
+
return [] unless aRing and rings_fields = self.respond_to?(:rings_fields).to_nil && self.rings_fields
|
|
112
|
+
|
|
113
|
+
fields = []
|
|
114
|
+
aRing.upto(rings_fields.length-1) do |i|
|
|
115
|
+
next unless ring_rec = rings_fields[i]
|
|
116
|
+
if af = ring_rec[aAbility.to_sym]
|
|
117
|
+
fields += af if af.is_a?(Array)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
fields.uniq!
|
|
121
|
+
fields.sort!
|
|
122
|
+
fields
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def permitted_fields(aAbility, aRing)
|
|
126
|
+
result = self.permitted(aAbility, aRing)
|
|
127
|
+
result.delete_if { |f| self.reflections.has_key? f }
|
|
128
|
+
result
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def permitted_associations(aAbility, aRing)
|
|
132
|
+
aRing = RingStrongParameters.lookup_ring(aRing)
|
|
133
|
+
return [] unless aRing and rings_fields = self.respond_to?(:rings_fields).to_nil && self.rings_fields
|
|
134
|
+
|
|
135
|
+
associations = self.reflections.keys
|
|
136
|
+
|
|
137
|
+
fields = []
|
|
138
|
+
aRing.upto(rings_fields.length-1) do |i|
|
|
139
|
+
next unless ring_rec = rings_fields[i]
|
|
140
|
+
if af = ring_rec[aAbility.to_sym]
|
|
141
|
+
fields += associations & af
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
fields.uniq!
|
|
145
|
+
fields.sort!
|
|
146
|
+
fields
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def ring_can?(aAbility, aRing)
|
|
150
|
+
aRing = RingStrongParameters.lookup_ring(aRing)
|
|
151
|
+
return [] unless aRing and rings_abilities = self.respond_to?(:rings_abilities).to_nil && self.rings_abilities
|
|
152
|
+
|
|
153
|
+
fields = []
|
|
154
|
+
aRing.upto(rings_abilities.length-1) do |i|
|
|
155
|
+
next unless ring_ab = rings_abilities[i]
|
|
156
|
+
return true if ring_ab.include?(aAbility)
|
|
157
|
+
end
|
|
158
|
+
return false
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
#module RingStrongParameters::Controller
|
|
166
|
+
#
|
|
167
|
+
# #def permitted(aAbility,aModel)
|
|
168
|
+
# # # lookup aModel.rings_fields and return fields that current_user can access with given ability
|
|
169
|
+
# # aModel = aModel.class if aModel.is_a? ActiveRecord::Base
|
|
170
|
+
# # ring = current_user.try(:ring)
|
|
171
|
+
# # aModel.permitted(aAbility,ring)
|
|
172
|
+
# #end
|
|
173
|
+
#
|
|
174
|
+
# #def permitted_fields(aAbility,aModel)
|
|
175
|
+
# # aModel = aModel.class if aModel.is_a? ActiveRecord::Base
|
|
176
|
+
# # ring = current_user.try(:ring)
|
|
177
|
+
# # aModel.permitted_fields(aAbility,ring)
|
|
178
|
+
# #end
|
|
179
|
+
#
|
|
180
|
+
# #def permitted_associations(aAbility,aModel)
|
|
181
|
+
# # aUser = current_user
|
|
182
|
+
# # aRing = aUser.try(:ring)
|
|
183
|
+
# # aModel = aModel.class if aModel.is_a? ActiveRecord::Base
|
|
184
|
+
# # aModel.permitted_associations(aAbility, aRing)
|
|
185
|
+
# #end
|
|
186
|
+
#
|
|
187
|
+
# #def ring_can?(aAbility,aModel)
|
|
188
|
+
# # aModel = aModel.class if aModel.is_a? ActiveRecord::Base
|
|
189
|
+
# # aAbility = aAbility.to_sym
|
|
190
|
+
# # ring = current_user.try(:ring)
|
|
191
|
+
# # aModel.ring_can?(aAbility, ring)
|
|
192
|
+
# #end
|
|
193
|
+
#
|
|
194
|
+
#end
|
|
195
|
+
|