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
@@ -0,0 +1,70 @@
|
|
1
|
+
namespace :db do
|
2
|
+
desc "create the database from config/database.yml from the current Sinatra env"
|
3
|
+
task :create do
|
4
|
+
ActiveRecordTasks.create()
|
5
|
+
end
|
6
|
+
|
7
|
+
desc "drops the data from config/database.yml from the current Sinatra env"
|
8
|
+
task :drop do
|
9
|
+
ActiveRecordTasks.drop()
|
10
|
+
end
|
11
|
+
|
12
|
+
desc "load the seed data from db/seeds.rb or run specific seed rake db:seed[file/path]"
|
13
|
+
task :seed, :file do |t, args|
|
14
|
+
ActiveRecordTasks.seed args[:file]
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "create the database and load the schema"
|
18
|
+
task :setup do
|
19
|
+
ActiveRecordTasks.setup()
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "create an ActiveRecord migration"
|
23
|
+
task :create_migration do
|
24
|
+
ActiveRecordTasks.create_migration(ENV["NAME"], ENV["VERSION"])
|
25
|
+
end
|
26
|
+
|
27
|
+
desc "migrate the database (use version with VERSION=n)"
|
28
|
+
task :migrate do
|
29
|
+
ActiveRecordTasks.migrate(ENV["VERSION"])
|
30
|
+
Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
|
31
|
+
end
|
32
|
+
|
33
|
+
desc "roll back the migration (use steps with STEP=n)"
|
34
|
+
task :rollback do
|
35
|
+
ActiveRecordTasks.rollback(ENV["STEP"])
|
36
|
+
Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
|
37
|
+
end
|
38
|
+
|
39
|
+
namespace :schema do
|
40
|
+
desc "dump schema into file"
|
41
|
+
task :dump do
|
42
|
+
ActiveRecordTasks.dump_schema()
|
43
|
+
end
|
44
|
+
|
45
|
+
desc "load schema into database"
|
46
|
+
task :load do
|
47
|
+
ActiveRecordTasks.load_schema()
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
namespace :test do
|
52
|
+
task :purge do
|
53
|
+
ActiveRecordTasks.with_config_environment 'test' do
|
54
|
+
ActiveRecordTasks.purge()
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
task :load => :purge do
|
59
|
+
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
|
60
|
+
ActiveRecordTasks.with_config_environment 'test' do
|
61
|
+
ActiveRecordTasks.load_schema()
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
desc 'Prepare test database from development schema'
|
66
|
+
task :prepare do
|
67
|
+
Rake::Task["db:test:load"].invoke
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_support/core_ext/string/strip'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'highline/import'
|
5
|
+
|
6
|
+
module ActiveRecordTasks
|
7
|
+
extend self
|
8
|
+
|
9
|
+
def create
|
10
|
+
silence_activerecord do
|
11
|
+
ActiveRecord::Tasks::DatabaseTasks.create(config)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def drop
|
16
|
+
silence_activerecord do
|
17
|
+
ActiveRecord::Tasks::DatabaseTasks.drop(config)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def seed file = false
|
22
|
+
if ENV['RACK_ENV'] == 'production'
|
23
|
+
continue = ask("Running seeds could override data, proceed y/N? ", String)
|
24
|
+
else
|
25
|
+
continue = 'y'
|
26
|
+
end
|
27
|
+
|
28
|
+
if %w(yes y Y).include? continue
|
29
|
+
silence_activerecord do
|
30
|
+
load("db/seeds.rb")
|
31
|
+
if not file
|
32
|
+
Seeds.run
|
33
|
+
else
|
34
|
+
load("db/seeds/#{file}.rb")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
else
|
38
|
+
say 'Aborting!'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def setup
|
43
|
+
silence_activerecord do
|
44
|
+
create()
|
45
|
+
load_schema()
|
46
|
+
seed()
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def create_migration(migration_name, version = nil)
|
51
|
+
raise "No NAME specified. Example usage: `rake db:create_migration NAME=create_users`" if migration_name.nil?
|
52
|
+
|
53
|
+
migration_number = version || Time.now.utc.strftime("%Y%m%d%H%M%S")
|
54
|
+
migration_file = File.join(migrations_dir, "#{migration_number}_#{migration_name}.rb")
|
55
|
+
migration_class = migration_name.split("_").map(&:capitalize).join
|
56
|
+
|
57
|
+
FileUtils.mkdir_p(migrations_dir)
|
58
|
+
File.open(migration_file, 'w') do |file|
|
59
|
+
file.write <<-MIGRATION.strip_heredoc
|
60
|
+
class #{migration_class} < ActiveRecord::Migration
|
61
|
+
def change
|
62
|
+
end
|
63
|
+
end
|
64
|
+
MIGRATION
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def migrate(version = nil)
|
69
|
+
silence_activerecord do
|
70
|
+
migration_version = version ? version.to_i : version
|
71
|
+
ActiveRecord::Migrator.migrate(migrations_dir, migration_version)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def rollback(step = nil)
|
76
|
+
silence_activerecord do
|
77
|
+
migration_step = step ? step.to_i : 1
|
78
|
+
ActiveRecord::Migrator.rollback(migrations_dir, migration_step)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def dump_schema(file_name = 'db/schema.rb')
|
83
|
+
silence_activerecord do
|
84
|
+
ActiveRecord::Migration.suppress_messages do
|
85
|
+
# Create file
|
86
|
+
out = File.new(file_name, 'w')
|
87
|
+
|
88
|
+
# Load schema
|
89
|
+
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, out)
|
90
|
+
|
91
|
+
out.close
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def purge
|
97
|
+
if config
|
98
|
+
ActiveRecord::Tasks::DatabaseTasks.purge(config)
|
99
|
+
else
|
100
|
+
raise ActiveRecord::ConnectionNotEstablished
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def load_schema(file_name = 'db/schema.rb')
|
105
|
+
load(file_name)
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def config
|
111
|
+
db = URI.parse ENV['DATABASE_URL']
|
112
|
+
|
113
|
+
{
|
114
|
+
adapter: db.scheme == 'postgres' ? 'postgresql' : db.scheme,
|
115
|
+
encoding: 'utf8',
|
116
|
+
reconnect: true,
|
117
|
+
database: db.path[1..-1],
|
118
|
+
host: db.host,
|
119
|
+
port: db.port,
|
120
|
+
pool: ENV['DATABASE_POOL'] || 5,
|
121
|
+
username: db.user,
|
122
|
+
password: db.password
|
123
|
+
}
|
124
|
+
end
|
125
|
+
|
126
|
+
def connect_to_active_record
|
127
|
+
ActiveRecord::Base.establish_connection config
|
128
|
+
end
|
129
|
+
|
130
|
+
def migrations_dir
|
131
|
+
ActiveRecord::Migrator.migrations_path
|
132
|
+
end
|
133
|
+
|
134
|
+
def silence_activerecord(&block)
|
135
|
+
connect_to_active_record
|
136
|
+
|
137
|
+
old_logger = ActiveRecord::Base.logger
|
138
|
+
ActiveRecord::Base.logger = nil
|
139
|
+
yield if block_given?
|
140
|
+
ActiveRecord::Base.logger = old_logger
|
141
|
+
end
|
142
|
+
end
|
data/lib/conjoin/version.rb
CHANGED
@@ -0,0 +1,323 @@
|
|
1
|
+
require "observer"
|
2
|
+
|
3
|
+
module Conjoin
|
4
|
+
module Widgets
|
5
|
+
class << self
|
6
|
+
attr_accessor :app
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.setup app
|
10
|
+
self.app = app
|
11
|
+
app.settings[:widgets_root] = "#{app.root}/app/widgets"
|
12
|
+
app.settings[:widgets] ||= {}
|
13
|
+
|
14
|
+
Dir["#{app.root}/app/widgets/**/*.rb"].each { |rb| require rb }
|
15
|
+
end
|
16
|
+
|
17
|
+
def widgets list = false
|
18
|
+
widget_name, incoming_event, event = load_widgets
|
19
|
+
|
20
|
+
if incoming_event
|
21
|
+
res.headers["Content-Type"] = "text/javascript; charset=utf-8"
|
22
|
+
event.trigger incoming_event.to_sym, widget_name, req.params
|
23
|
+
res.write "$('head > meta[name=csrf-token]').attr('content', '#{csrf_token}');"
|
24
|
+
res.write '$(document).trigger("page:change");'
|
25
|
+
end
|
26
|
+
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
def widgets_root
|
31
|
+
settings[:widgets_root]
|
32
|
+
end
|
33
|
+
|
34
|
+
def render_widget *args
|
35
|
+
load_widgets unless req.env[:loaded_widgets]
|
36
|
+
|
37
|
+
if args.first.kind_of? Hash
|
38
|
+
opts = args.first
|
39
|
+
name = req.env[:widget_name]
|
40
|
+
else
|
41
|
+
name = args.first
|
42
|
+
opts = args.length > 1 ? args.last : {}
|
43
|
+
end
|
44
|
+
|
45
|
+
# set the current state (the method that will get called on render_widget)
|
46
|
+
state = opts[:state] || 'display'
|
47
|
+
|
48
|
+
widget = req.env[:loaded_widgets][name]
|
49
|
+
|
50
|
+
if widget.method(state).parameters.length > 0
|
51
|
+
widget.send state, opts
|
52
|
+
else
|
53
|
+
widget.send state
|
54
|
+
end
|
55
|
+
end
|
56
|
+
alias_method :widget_render, :render_widget
|
57
|
+
|
58
|
+
def widget_div opts = {}, &block
|
59
|
+
defaults = {
|
60
|
+
id: "#{req.env[:widget_name]}_#{req.env[:widget_state]}"
|
61
|
+
}.merge opts
|
62
|
+
|
63
|
+
name = req.env[:widget_name]
|
64
|
+
widget = req.env[:loaded_widgets][name]
|
65
|
+
|
66
|
+
html = block.call(widget)
|
67
|
+
|
68
|
+
mab do
|
69
|
+
div defaults do
|
70
|
+
text! html
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def url_for_event event, options = {}
|
76
|
+
widget_name = req.env[:widget_name]
|
77
|
+
"#{path_for('widgets')}?widget_event=#{event}&widget_name=#{widget_name}&" + URI.encode_www_form(options)
|
78
|
+
end
|
79
|
+
|
80
|
+
def load_widgets
|
81
|
+
req.env[:loaded_widgets] ||= {}
|
82
|
+
|
83
|
+
event = Events.new res, req, settings
|
84
|
+
|
85
|
+
if incoming_event = req.params["widget_event"]
|
86
|
+
widget_name = req.params["widget_name"]
|
87
|
+
opts = { from_event: true }
|
88
|
+
else
|
89
|
+
opts = {}
|
90
|
+
end
|
91
|
+
|
92
|
+
settings[:widgets].each do |name, widget|
|
93
|
+
req.env[:loaded_widgets][name] = widget.constantize.new(self, res, req, settings, event, name, opts)
|
94
|
+
end
|
95
|
+
|
96
|
+
[widget_name, incoming_event, event]
|
97
|
+
end
|
98
|
+
|
99
|
+
module ClassMethods
|
100
|
+
def has_widgets *list
|
101
|
+
list.each do |widget|
|
102
|
+
settings[:widgets].merge! widget
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def widgets_root= path
|
107
|
+
settings[:widgets_root] = path
|
108
|
+
end
|
109
|
+
|
110
|
+
def widgets_root
|
111
|
+
settings[:widgets_root]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class Events < Struct.new(:res, :req, :settings)
|
116
|
+
include Observable
|
117
|
+
|
118
|
+
def trigger event, widget_name, user_data = {}
|
119
|
+
# data = OpenStruct.new({
|
120
|
+
# settings: settings,
|
121
|
+
# data: user_data
|
122
|
+
# })
|
123
|
+
data = user_data.to_ostruct
|
124
|
+
|
125
|
+
# THIS IS WHAT WILL MAKE SURE EVENTS ARE TRIGGERED
|
126
|
+
changed
|
127
|
+
##################################################
|
128
|
+
|
129
|
+
notify_observers event, widget_name, data
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
class Base
|
134
|
+
JS_ESCAPE = { '\\' => '\\\\', '</' => '<\/', "\r\n" => '\n', "\n" => '\n', "\r" => '\n', '"' => '\\"', "'" => "\\'" }
|
135
|
+
attr_accessor :app, :res, :req, :settings, :event, :folder, :options
|
136
|
+
|
137
|
+
def initialize app, res, req, settings, event, folder, options
|
138
|
+
@app = app
|
139
|
+
@res = res
|
140
|
+
@req = req
|
141
|
+
@settings = settings
|
142
|
+
@event = event
|
143
|
+
@folder = folder
|
144
|
+
@options = options
|
145
|
+
|
146
|
+
# add the widget to the req widgets
|
147
|
+
req.env[:widgets] ||= {}
|
148
|
+
unless req.env[:widgets][folder]
|
149
|
+
req.env[:widgets][folder] = {}
|
150
|
+
end
|
151
|
+
|
152
|
+
event.add_observer self, :trigger_event
|
153
|
+
end
|
154
|
+
|
155
|
+
def current_user
|
156
|
+
app.current_user
|
157
|
+
end
|
158
|
+
|
159
|
+
def id_for state
|
160
|
+
"#{req.env[:widget_name]}_#{state}"
|
161
|
+
end
|
162
|
+
|
163
|
+
def page_change
|
164
|
+
res.headers["Content-Type"] = "text/javascript; charset=utf-8"
|
165
|
+
res.write '$(document).trigger("page:change");'
|
166
|
+
end
|
167
|
+
|
168
|
+
def replace state, opts = {}
|
169
|
+
@options[:replace] = true
|
170
|
+
|
171
|
+
if !state.is_a? String
|
172
|
+
content = render state, opts
|
173
|
+
selector = '#' + id_for(state)
|
174
|
+
else
|
175
|
+
if !opts.key?(:content) and !opts.key?(:with)
|
176
|
+
content = render opts
|
177
|
+
else
|
178
|
+
content = opts[:content] || opts[:with]
|
179
|
+
end
|
180
|
+
selector = state
|
181
|
+
end
|
182
|
+
|
183
|
+
res.write '$("' + selector + '").replaceWith("' + escape(content) + '");'
|
184
|
+
# scroll to the top of the page just as if we went to the url directly
|
185
|
+
res.write 'window.scrollTo(0, 0);'
|
186
|
+
end
|
187
|
+
|
188
|
+
def hide selector
|
189
|
+
res.write '$("' + selector + '").hide();'
|
190
|
+
end
|
191
|
+
|
192
|
+
def append selector, opts = {}
|
193
|
+
content = render opts
|
194
|
+
res.write '$("' + selector + '").append("'+ escape(content) +'");'
|
195
|
+
end
|
196
|
+
|
197
|
+
def add_after selector, opts = {}
|
198
|
+
content = render opts
|
199
|
+
res.write '$("' + selector + '").after("'+ escape(content) +'");'
|
200
|
+
end
|
201
|
+
|
202
|
+
def attrs_for selector, opts = {}
|
203
|
+
res.write '$("' + selector + '").attr('+ (opts.to_json) +');'
|
204
|
+
end
|
205
|
+
|
206
|
+
def escape js
|
207
|
+
js.to_s.gsub(/(\\|<\/|\r\n|\\3342\\2200\\2250|[\n\r"'])/) {|match| JS_ESCAPE[match] }
|
208
|
+
end
|
209
|
+
|
210
|
+
def trigger_event event, widget_name, data = {}
|
211
|
+
if events = self.class.events
|
212
|
+
events.each do |class_event, opts|
|
213
|
+
if class_event == event && (widget_name == folder.to_s or opts[:for].to_s == widget_name)
|
214
|
+
event = opts[:with] if opts[:with]
|
215
|
+
|
216
|
+
if method(event).parameters.length > 0
|
217
|
+
send(event, data)
|
218
|
+
else
|
219
|
+
send(event)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def render *args
|
227
|
+
if args.first.kind_of? Hash
|
228
|
+
locals = args.first
|
229
|
+
# if it's a partial we add an underscore infront of it
|
230
|
+
state = view = locals[:state] || "#{locals[:partial]}".gsub(/([a-zA-Z_]+)$/, '_\1')
|
231
|
+
else
|
232
|
+
state = view = args.first
|
233
|
+
locals = args.length > 1 ? args.last : {}
|
234
|
+
end
|
235
|
+
|
236
|
+
unless view
|
237
|
+
state = view = caller[0][/`.*'/][1..-2]
|
238
|
+
|
239
|
+
if (options.key?(:from_event) and !options.key?(:replace))
|
240
|
+
@options[:cache] = false
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
if locals.key?(:state) and state.to_s == view
|
245
|
+
return send state
|
246
|
+
end
|
247
|
+
|
248
|
+
tmpl_engine = settings[:render][:template_engine]
|
249
|
+
|
250
|
+
if (req_helper_methods = req.env[:widgets][folder][:req_helper_methods]) \
|
251
|
+
and (!options.key?(:cache))
|
252
|
+
locals.merge! req_helper_methods
|
253
|
+
else
|
254
|
+
req.env[:widgets][folder][:req_helper_methods] = {}
|
255
|
+
|
256
|
+
helper_methods.each do |method|
|
257
|
+
req.env[:widgets][folder][:req_helper_methods][method] = locals[method] = self.send method
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
req.env[:widget_name] = folder
|
262
|
+
req.env[:widget_state] = state
|
263
|
+
|
264
|
+
locals[:w] = locals[:widget] = self
|
265
|
+
|
266
|
+
app.render "#{app.widgets_root}/#{folder}/#{view}.#{tmpl_engine}", locals
|
267
|
+
end
|
268
|
+
|
269
|
+
private
|
270
|
+
|
271
|
+
def helper_methods
|
272
|
+
self.class.available_helper_methods || []
|
273
|
+
end
|
274
|
+
|
275
|
+
class << self
|
276
|
+
attr_accessor :events, :available_helper_methods
|
277
|
+
|
278
|
+
def respond_to event, opts = {}
|
279
|
+
@events ||= []
|
280
|
+
@events << [event, opts]
|
281
|
+
end
|
282
|
+
|
283
|
+
def responds_to *events
|
284
|
+
@events ||= []
|
285
|
+
events.each do |event|
|
286
|
+
@events << [event, {}]
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def helper_method method
|
291
|
+
@available_helper_methods ||= []
|
292
|
+
@available_helper_methods << method
|
293
|
+
end
|
294
|
+
|
295
|
+
def helper_methods *methods
|
296
|
+
methods.each do |method|
|
297
|
+
helper_method method
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
class Routes < Struct.new(:settings)
|
304
|
+
def app
|
305
|
+
App.settings = settings
|
306
|
+
App.plugin Conjoin::Cuba::Render
|
307
|
+
App.plugin Conjoin::Auth
|
308
|
+
App.plugin Conjoin::Assets
|
309
|
+
App.plugin Conjoin::I18N
|
310
|
+
App.plugin Conjoin::FormBuilder
|
311
|
+
App.plugin Conjoin::Widgets
|
312
|
+
|
313
|
+
App
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
class App < Conjoin::Cuba
|
318
|
+
define do
|
319
|
+
on(default, widgets) {}
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|