conjoin 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|