conjoin 0.0.1 → 0.0.2
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 +4 -4
- data/conjoin.gemspec +20 -0
- data/lib/conjoin.rb +27 -1
- data/lib/conjoin/active_record.rb +374 -0
- data/lib/conjoin/assets.rb +205 -0
- data/lib/conjoin/auth.rb +92 -0
- data/lib/conjoin/class_methods.rb +15 -0
- data/lib/conjoin/csrf.rb +26 -0
- data/lib/conjoin/cuba.rb +72 -0
- data/lib/conjoin/env_string.rb +17 -0
- data/lib/conjoin/form_builder.rb +334 -0
- data/lib/conjoin/i18n.rb +97 -0
- data/lib/conjoin/inputs/boolean.rb +8 -0
- data/lib/conjoin/inputs/checkbox.rb +14 -0
- data/lib/conjoin/inputs/date.rb +11 -0
- data/lib/conjoin/inputs/decimal.rb +9 -0
- data/lib/conjoin/inputs/file.rb +17 -0
- data/lib/conjoin/inputs/hidden.rb +10 -0
- data/lib/conjoin/inputs/integer.rb +9 -0
- data/lib/conjoin/inputs/password.rb +10 -0
- data/lib/conjoin/inputs/radio.rb +35 -0
- data/lib/conjoin/inputs/select.rb +67 -0
- data/lib/conjoin/inputs/state.rb +68 -0
- data/lib/conjoin/inputs/string.rb +9 -0
- data/lib/conjoin/inputs/time.rb +11 -0
- data/lib/conjoin/middleware.rb +40 -0
- data/lib/conjoin/recursive_ostruct.rb +117 -0
- data/lib/conjoin/tasks.rb +4 -0
- data/lib/conjoin/tasks/migrate.rake +70 -0
- data/lib/conjoin/tasks/migrate.rb +142 -0
- data/lib/conjoin/version.rb +1 -1
- data/lib/conjoin/widgets.rb +323 -0
- metadata +296 -2
data/lib/conjoin/auth.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
module Conjoin
|
2
|
+
module Auth
|
3
|
+
if Conjoin.env.mounted?
|
4
|
+
require 'warden'
|
5
|
+
require 'devise'
|
6
|
+
include Devise::TestHelpers
|
7
|
+
end
|
8
|
+
|
9
|
+
class << self
|
10
|
+
attr_accessor :app
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.setup app
|
14
|
+
self.app = app
|
15
|
+
|
16
|
+
if not Conjoin.env.mounted?
|
17
|
+
require 'shield'
|
18
|
+
app.plugin Shield::Helpers
|
19
|
+
app.use Shield::Middleware, "/login"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def login_path
|
24
|
+
req.env['REQUEST_URI'][/login/] ? true : false
|
25
|
+
end
|
26
|
+
|
27
|
+
def logout_path
|
28
|
+
req.env['REQUEST_URI'][/logout/] ? true : false
|
29
|
+
end
|
30
|
+
|
31
|
+
def current_user
|
32
|
+
if not Conjoin.env.mounted?
|
33
|
+
authenticated(VendorWizard::User)
|
34
|
+
else
|
35
|
+
req.env['warden'].authenticate(scope: :user)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def logged_in?
|
40
|
+
current_user ? true : false
|
41
|
+
end
|
42
|
+
|
43
|
+
def sign_in *args
|
44
|
+
if args.length > 1
|
45
|
+
user, scope = args
|
46
|
+
else
|
47
|
+
scope = :user
|
48
|
+
user = args.first
|
49
|
+
end
|
50
|
+
|
51
|
+
if Auth.app.mounted?
|
52
|
+
@request = req
|
53
|
+
super scope, user
|
54
|
+
else
|
55
|
+
session.clear
|
56
|
+
session['VendorWizard::User'] = user.id
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class Routes < Conjoin::Cuba
|
61
|
+
plugin Auth
|
62
|
+
|
63
|
+
define do
|
64
|
+
on login_path do
|
65
|
+
on get do
|
66
|
+
user = UserLoginForm.new
|
67
|
+
res.write view("login", user: user)
|
68
|
+
end
|
69
|
+
|
70
|
+
on post do
|
71
|
+
user = UserLoginForm.new
|
72
|
+
user.attributes = req[:user_login]
|
73
|
+
|
74
|
+
if user.valid_only?(:email, :password) && login(User, user.email, user.password)
|
75
|
+
res.redirect req[:return] || '/'
|
76
|
+
else
|
77
|
+
user.password = nil
|
78
|
+
end
|
79
|
+
|
80
|
+
res.write view("login", user: user)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
on logout_path do
|
85
|
+
logout User
|
86
|
+
|
87
|
+
res.redirect '/'
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/conjoin/csrf.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require "rack/csrf"
|
2
|
+
|
3
|
+
module Conjoin
|
4
|
+
module Csrf
|
5
|
+
# Public: Sugar to include a csrf tag
|
6
|
+
#
|
7
|
+
# Examples:
|
8
|
+
#
|
9
|
+
# <form action="/new">
|
10
|
+
# <%= csrf_tag %>
|
11
|
+
# <input type="text" />
|
12
|
+
# </form>
|
13
|
+
def csrf_tag
|
14
|
+
Rack::Csrf.tag(env)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Public: Sugar to access the csrf token
|
18
|
+
#
|
19
|
+
# Examples:
|
20
|
+
#
|
21
|
+
# <%= csrf_token %>
|
22
|
+
def csrf_token
|
23
|
+
Rack::Csrf.token(env)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/conjoin/cuba.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require "cuba"
|
2
|
+
require "cuba/render"
|
3
|
+
|
4
|
+
module Conjoin
|
5
|
+
class Cuba < ::Cuba
|
6
|
+
class << self
|
7
|
+
def settings= settings
|
8
|
+
@settings = settings
|
9
|
+
end
|
10
|
+
|
11
|
+
def root; @root ||= Dir.pwd; end
|
12
|
+
def root= root
|
13
|
+
@root = root
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize!
|
17
|
+
# Initializers
|
18
|
+
Dir["#{root}/config/initializers/**/*.rb"].each { |rb| require rb }
|
19
|
+
|
20
|
+
# Permissions
|
21
|
+
Dir["#{root}/app/models/permissions/**/*.rb"].each {|rb| require rb }
|
22
|
+
Dir["#{root}/app/permissions/**/*.rb"].each {|rb| require rb }
|
23
|
+
|
24
|
+
# Models
|
25
|
+
Dir["#{root}/app/models/*/*.rb"].each {|rb| require rb }
|
26
|
+
Dir["#{root}/app/models/**/*.rb"].each {|rb| require rb }
|
27
|
+
|
28
|
+
# Forms
|
29
|
+
Dir["#{root}/app/forms/*/*.rb"].each {|rb| require rb }
|
30
|
+
Dir["#{root}/app/forms/**/*.rb"].each {|rb| require rb }
|
31
|
+
|
32
|
+
# Assets
|
33
|
+
require "#{root}/config/assets"
|
34
|
+
|
35
|
+
# Presenters
|
36
|
+
Dir["#{root}/app/presenters/**/*.rb"].each { |rb| require rb }
|
37
|
+
|
38
|
+
# Mailers
|
39
|
+
Dir["#{root}/app/mailers/**/*.rb"].each { |rb| require rb }
|
40
|
+
|
41
|
+
# Routes
|
42
|
+
Dir["#{root}/app/routes/**/*.rb"].each { |rb| require rb }
|
43
|
+
require "#{root}/config/routes"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
module Render
|
48
|
+
include ::Cuba::Render
|
49
|
+
|
50
|
+
def self.setup(app)
|
51
|
+
app.settings[:render] ||= {}
|
52
|
+
app.settings[:render][:template_engine] ||= "slim"
|
53
|
+
app.settings[:render][:layout] ||= "layouts/app"
|
54
|
+
app.settings[:render][:views] ||= "#{app.root}/app/views"
|
55
|
+
app.settings[:render][:options] ||= {
|
56
|
+
default_encoding: Encoding.default_external
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
alias original_partial partial
|
61
|
+
|
62
|
+
def view(template, locals = {}, layout = settings[:render][:layout])
|
63
|
+
original_partial(layout, { content: original_partial(template, locals) }.merge(locals))
|
64
|
+
end
|
65
|
+
|
66
|
+
def partial template, locals = {}
|
67
|
+
partial_template = template.gsub(/([a-zA-Z_]+)$/, '_\1')
|
68
|
+
render(template_path(partial_template), locals, settings[:render][:options])
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Conjoin
|
2
|
+
class EnvString < String
|
3
|
+
[:production, :development, :test, :staging].each do |env|
|
4
|
+
define_method "#{env}?" do
|
5
|
+
self == env.to_s
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def mounted?
|
10
|
+
defined?(::Rails) ? true : false
|
11
|
+
end
|
12
|
+
|
13
|
+
def console?
|
14
|
+
ENV['CONJOIN_CONSOLE'] ? true : false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,334 @@
|
|
1
|
+
require "rack/csrf"
|
2
|
+
|
3
|
+
module Conjoin
|
4
|
+
module FormBuilder
|
5
|
+
INPUTS = [
|
6
|
+
:boolean, :checkbox, :date, :decimal, :file, :hidden,
|
7
|
+
:integer, :password, :radio, :select, :state, :string,
|
8
|
+
:time
|
9
|
+
]
|
10
|
+
|
11
|
+
def self.setup app
|
12
|
+
require 'mab/kernel_method'
|
13
|
+
|
14
|
+
app.use Rack::Csrf
|
15
|
+
app.plugin Conjoin::Csrf
|
16
|
+
# Dir["#{File.dirname(__FILE__)}/plugin/inputs/**/*.rb"].each { |rb| require rb }
|
17
|
+
INPUTS.each do |input|
|
18
|
+
require_relative "inputs/#{input}"
|
19
|
+
end
|
20
|
+
|
21
|
+
Dir["#{app.root}/app/inputs/**/*.rb"].each { |rb| require rb }
|
22
|
+
end
|
23
|
+
|
24
|
+
def form_for record, options = {}, &block
|
25
|
+
raise ArgumentError, "Missing block" unless block_given?
|
26
|
+
|
27
|
+
if as = options.delete(:as)
|
28
|
+
model_name = as
|
29
|
+
elsif record.is_form?
|
30
|
+
model_name = record.class.model_name.to_s.gsub(/\w+::/, '').gsub(/Form$/, '').underscore
|
31
|
+
else
|
32
|
+
model_name = record.class.model_name.singular
|
33
|
+
end
|
34
|
+
|
35
|
+
fields = Fields.new self, [model_name], record, block
|
36
|
+
|
37
|
+
form_options = {
|
38
|
+
class: 'form-horizontal',
|
39
|
+
role: 'form',
|
40
|
+
method: 'post',
|
41
|
+
novalidate: 'true',
|
42
|
+
remote: true,
|
43
|
+
action: options.delete(:url) || "/" + record.class.model_name.plural
|
44
|
+
}.merge! options
|
45
|
+
|
46
|
+
if form_options.delete(:remote)
|
47
|
+
form_options['data-remote'] = 'true'
|
48
|
+
end
|
49
|
+
|
50
|
+
mab do
|
51
|
+
form form_options do
|
52
|
+
input type: 'hidden', name: '_method', value: (record.id ? 'patch' : 'post')
|
53
|
+
text! csrf_tag
|
54
|
+
text! fields.render
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Fields < Struct.new(:app, :models, :record, :block, :index)
|
60
|
+
def render
|
61
|
+
html = block.call(self, index)
|
62
|
+
names = [].concat models
|
63
|
+
names << 'id'
|
64
|
+
|
65
|
+
mab do
|
66
|
+
if record.id
|
67
|
+
input type: 'hidden', name: nested_names_for(names), value: record.id
|
68
|
+
end
|
69
|
+
text! html
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def submit options = {}
|
74
|
+
mab do
|
75
|
+
input type: 'submit',
|
76
|
+
value: options[:value] || 'Submit',
|
77
|
+
class: 'btn'
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def nested_names_for names
|
82
|
+
# create field names that map to the correct models
|
83
|
+
names.each_with_index.map do |field, i|
|
84
|
+
i != 0 ? "[#{field}]" : field
|
85
|
+
end.join
|
86
|
+
end
|
87
|
+
|
88
|
+
def association field_name, options = {}
|
89
|
+
options[:is_association] = true
|
90
|
+
input field_name, options
|
91
|
+
end
|
92
|
+
|
93
|
+
def input_field field_name, options = {}
|
94
|
+
options[:wrapper] = false
|
95
|
+
input field_name, options
|
96
|
+
end
|
97
|
+
|
98
|
+
def input field_name, options = {}
|
99
|
+
names = [].concat models
|
100
|
+
if options.delete(:is_association)
|
101
|
+
names << "#{field_name.to_s.singularize}_ids"
|
102
|
+
names << ''
|
103
|
+
else
|
104
|
+
names << field_name
|
105
|
+
end
|
106
|
+
|
107
|
+
# create field names that map to the correct models
|
108
|
+
nested_name = nested_names_for names
|
109
|
+
|
110
|
+
record_class = record.class.model_name.name.constantize
|
111
|
+
|
112
|
+
if as = options.delete(:as)
|
113
|
+
record_type = as.to_s.classify
|
114
|
+
elsif record_class.mini_record_columns \
|
115
|
+
and mini_column = record_class.mini_record_columns[field_name] \
|
116
|
+
and input_as = mini_column[:input_as]
|
117
|
+
record_type = input_as.to_s.classify
|
118
|
+
else
|
119
|
+
record_type = record_class.columns_hash[field_name.to_s].type.to_s.classify
|
120
|
+
end
|
121
|
+
|
122
|
+
if mini_column and opts = mini_column[:input_options]
|
123
|
+
options = opts.merge options
|
124
|
+
end
|
125
|
+
|
126
|
+
input_class = "Conjoin::FormBuilder::#{record_type}Input".constantize
|
127
|
+
|
128
|
+
data = OpenStruct.new({
|
129
|
+
name: nested_name,
|
130
|
+
record: record,
|
131
|
+
value: record.send(field_name),
|
132
|
+
options: options,
|
133
|
+
errors: record.errors.messages[field_name],
|
134
|
+
names: names
|
135
|
+
})
|
136
|
+
|
137
|
+
new_input = input_class.new data, app, record
|
138
|
+
|
139
|
+
if record_type != 'Hidden' \
|
140
|
+
and not options.key(:wrapper) and options[:wrapper] != false
|
141
|
+
wrapper field_name.to_s, nested_name, new_input, options
|
142
|
+
else
|
143
|
+
new_input.render
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def fields_for field_name, options = {}, &block
|
148
|
+
names = [].concat models
|
149
|
+
|
150
|
+
associated_record = record.send field_name
|
151
|
+
|
152
|
+
if scope = options.delete(:scope)
|
153
|
+
associated_record = associated_record.send(scope, *options.delete(:scope_args))
|
154
|
+
end
|
155
|
+
|
156
|
+
if select = options.delete(:select)
|
157
|
+
associated_record = Hash[associated_record.each_with_index.map {|a, i| [i, a]}]
|
158
|
+
|
159
|
+
select.each do |key, select_array|
|
160
|
+
select_array = [select_array] unless select_array.is_a? Array
|
161
|
+
|
162
|
+
associated_record.select! do |k, v|
|
163
|
+
select_array.include? :"#{v[key]}"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
if name = options.delete(:name)
|
169
|
+
field_name = name
|
170
|
+
end
|
171
|
+
|
172
|
+
names << "#{field_name}_attributes"
|
173
|
+
|
174
|
+
if !associated_record.kind_of? ActiveRecord::Associations::CollectionProxy \
|
175
|
+
and !associated_record.kind_of? ActiveRecord::AssociationRelation \
|
176
|
+
and !associated_record.kind_of? Array \
|
177
|
+
and !associated_record.kind_of? Hash
|
178
|
+
fields = Fields.new app, names, associated_record, block, 0
|
179
|
+
fields.render
|
180
|
+
else
|
181
|
+
html = ''
|
182
|
+
if associated_record.kind_of? Array
|
183
|
+
associated_record.each_with_index do |current_record, i|
|
184
|
+
nested_names = [].concat names
|
185
|
+
nested_names << i
|
186
|
+
|
187
|
+
fields = Fields.new app, nested_names, current_record, block, i
|
188
|
+
html += fields.render
|
189
|
+
end
|
190
|
+
else
|
191
|
+
associated_record.each do |i, current_record|
|
192
|
+
nested_names = [].concat names
|
193
|
+
nested_names << i
|
194
|
+
|
195
|
+
fields = Fields.new app, nested_names, current_record, block, i
|
196
|
+
html += fields.render
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
html
|
201
|
+
end
|
202
|
+
|
203
|
+
# rescue
|
204
|
+
# raise "No associated record #{field_name} for #{record.class}"
|
205
|
+
end
|
206
|
+
|
207
|
+
def errors_for attr
|
208
|
+
mab do
|
209
|
+
span class: 'has-error has-feedback form-control-feedback' do
|
210
|
+
text! record.errors.messages[attr.to_sym].try :join, ', '
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
private
|
216
|
+
|
217
|
+
def id_for field_name
|
218
|
+
field_name.gsub(/[^a-z0-9]/, '_').gsub(/__/, '_').gsub(/_$/, '')
|
219
|
+
end
|
220
|
+
|
221
|
+
def required?(obj, attr, options)
|
222
|
+
if options.key?(:required)
|
223
|
+
options[:required]
|
224
|
+
else
|
225
|
+
target = (obj.class == Class) ? obj : obj.class
|
226
|
+
presence = target.validators_on(attr).select { |t| t.class.to_s == 'ActiveRecord::Validations::PresenceValidator' }
|
227
|
+
if presence.any?
|
228
|
+
is_required = true
|
229
|
+
|
230
|
+
presence.each do |p|
|
231
|
+
if p.options[:if]
|
232
|
+
is_required &= p.options[:if].call(record)
|
233
|
+
end
|
234
|
+
|
235
|
+
if p.options[:unless]
|
236
|
+
is_required &= !p.options[:if].call(record)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
else
|
240
|
+
is_required = false
|
241
|
+
end
|
242
|
+
|
243
|
+
is_required
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def errors? obj, attr
|
248
|
+
obj.errors.messages[attr.to_sym]
|
249
|
+
end
|
250
|
+
|
251
|
+
def wrapper field_name, nested_name, input, options
|
252
|
+
if w = options[:wrapper] and w.is_a? String
|
253
|
+
label_width, input_width = w.split ','
|
254
|
+
else
|
255
|
+
label_width, input_width = [3, 9]
|
256
|
+
end
|
257
|
+
|
258
|
+
mab do
|
259
|
+
div class: "form-group #{errors?(record, field_name) ? 'has-error has-feedback' : ''}" do
|
260
|
+
label for: id_for(nested_name), class: "control-label col-sm-#{label_width}" do
|
261
|
+
if required? record, field_name, options
|
262
|
+
abbr title: 'required' do
|
263
|
+
text '*'
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
i18n_s = nested_name.gsub(/.+\[([a-z\_]+)\](?:.*|)\[([a-z\_]+)\]$/, 'model.\1.\2').gsub(/_attributes/, '')
|
268
|
+
|
269
|
+
i18n_name = options[:label] || R18n.t(i18n_s.gsub(/^model/, 'form.label')) | R18n.t.form.label[field_name] | R18n.t(i18n_s) | field_name.titleize
|
270
|
+
|
271
|
+
text! i18n_name
|
272
|
+
end
|
273
|
+
div class: "col-sm-#{input_width}" do
|
274
|
+
text! input.render
|
275
|
+
if errors = errors?(record, field_name)
|
276
|
+
span class: 'help-block has-error' do
|
277
|
+
text errors.join ', '
|
278
|
+
end
|
279
|
+
span class: 'fa fa-times form-control-feedback'
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
class Input
|
288
|
+
attr_accessor :app, :data, :options, :record
|
289
|
+
|
290
|
+
def initialize data, app, record
|
291
|
+
@data = data
|
292
|
+
@app = app
|
293
|
+
@record = record
|
294
|
+
@options = {
|
295
|
+
name: data.name,
|
296
|
+
type: :text,
|
297
|
+
id: id,
|
298
|
+
value: data.value,
|
299
|
+
class: ''
|
300
|
+
}.merge! data.options
|
301
|
+
options[:class] += ' form-control'
|
302
|
+
@options
|
303
|
+
end
|
304
|
+
|
305
|
+
def id
|
306
|
+
data.name.gsub(/[^a-z0-9]/, '_').gsub(/__/, '_').gsub(/_$/, '')
|
307
|
+
end
|
308
|
+
|
309
|
+
def nested_name
|
310
|
+
# create field names that map to the correct models
|
311
|
+
data.names.each_with_index.map do |field, i|
|
312
|
+
i != 0 ? "[#{field}]" : field
|
313
|
+
end.join
|
314
|
+
end
|
315
|
+
|
316
|
+
def errors?
|
317
|
+
data.errors
|
318
|
+
end
|
319
|
+
|
320
|
+
def render
|
321
|
+
if options[:type] == :hidden \
|
322
|
+
or (options.key?(:wrapper) and options[:wrapper] == false)
|
323
|
+
options[:class] = options[:class].gsub(/form-control/, '')
|
324
|
+
end
|
325
|
+
|
326
|
+
display
|
327
|
+
end
|
328
|
+
|
329
|
+
def display
|
330
|
+
mab { input options }
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|