snails 0.0.8 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: daf351c2597c83b274ecc9a36cd7bd8558b96e0e
4
- data.tar.gz: eaa66e198d7d3b20cad52442d8999fff954ed060
3
+ metadata.gz: 88f932ce0b002068d85f97f21f87fd7250473f46
4
+ data.tar.gz: 746a134507fb00de2b222388b8124b5837a2b474
5
5
  SHA512:
6
- metadata.gz: 2bb1cc458d7d5120ebc098ff6641013e7fba9ff18ce57ca12e86269189b1a2b741f9f94362298a518d9cd1b81841d9ea38548730c625ecb763ea98641a76b4f9
7
- data.tar.gz: 6082675242f0a8295e3a10a7f234f18b6a5d29e6274533985ea1e8cf09be38dd7aef653b2fa0e273bb82b187455ae3b91f46b543524529d9447aa0f631cb7432
6
+ metadata.gz: ab90d572eeaf226d8fd25b3e604ec47afa2e3a54638d827d753900c74a87a75a7facd5ccdab7864258e23e8e62c7a10d4c8b70ecd7eaeb71e7d73687fde81a5a
7
+ data.tar.gz: 50f360665268d2787a3b2df14750425228bf29e7b1a46ab398212ee93fe334908550ca616b067bcbb564ba4da265d4781ee9d3d195b09271da39e07776e726d8
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
+ *.gem
1
2
  pkg
2
3
  test
3
4
  example/Gemfile.lock
@@ -1,13 +1,11 @@
1
- %w(
2
- logger
3
- active_support/string_inquirer
4
- active_support/core_ext/hash
5
- sinatra/base
6
- sinatra/content_for
7
- sinatra/flash
8
- ).each { |lib| require lib }
1
+ require 'logger'
2
+ require 'snails/app'
9
3
 
10
4
  module Snails
5
+ def self.root
6
+ @root ||= Pathname.new(Dir.pwd)
7
+ end
8
+
11
9
  def self.env
12
10
  @env ||= ActiveSupport::StringInquirer.new(ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development')
13
11
  end
@@ -20,480 +18,8 @@ module Snails
20
18
  puts "Warning: There's more than one Snail app defined!" if @apps.count > 1
21
19
  @apps.first
22
20
  end
23
- end
24
-
25
- module Snails
26
-
27
- module RequiredParams
28
- def requires!(req, hash = params)
29
- if req.is_a?(Hash)
30
- req.each do |k, vals|
31
- if vals.is_a?(Array) or vals.is_a?(Hash)
32
- halt(400, "Missing: #{k} in #{hash}") if hash[k].nil?
33
- requires!(vals, hash[k])
34
- else
35
- requires!(k, hash)
36
- end
37
- end
38
- elsif req.nil? or (req.is_a?(Symbol) and hash[req].nil?) \
39
- or (req.is_a?(Array) and req.any? { |p| hash[p].nil? })
40
- halt(400, "Required parameters: #{req} (in #{hash})")
41
- end
42
- end
43
- end
44
-
45
- class App < Sinatra::Base
46
-
47
- def self.inherited(base)
48
- Snails.apps << base
49
- super
50
- end
51
-
52
- cwd = Pathname.new(Dir.pwd) # settings.root
53
- LOGGER = Logger.new(File.exist?(cwd.join('log')) ? cwd.join('log', "#{Snails.env}.log") : nil)
54
-
55
- set :protection, except: :frame_options
56
- set :views, cwd.join('lib', 'views')
57
- set :session_secret, ENV.fetch('SESSION_SECRET') { SecureRandom.hex }
58
- set :static_paths, %w(/css /img /js /files /fonts favicon.ico)
59
-
60
- enable :sessions
61
- enable :method_override
62
- enable :logging
63
-
64
- register Sinatra::Flash
65
- use Rack::CommonLogger, LOGGER
66
- use Rack::Static, urls: static_paths, root: 'public'
67
-
68
- configure :production, :staging do
69
- set :raise_errors, true
70
- set :dump_errors, false
71
- end
72
-
73
- configure :development do
74
- set :raise_errors, true
75
- set :show_exceptions, true
76
- set :log_level, Logger::DEBUG
77
- end
78
-
79
- helpers do
80
- include RequiredParams
81
- include Sinatra::ContentFor
82
- def logger; LOGGER; end
83
- end
84
-
85
- error do
86
- err = request.env['sinatra.error']
87
- logger.error err.message
88
- logger.error err.backtrace.first(3).join("\n")
89
- halt(500, err.message)
90
- end
91
-
92
- not_found do
93
- show_error(404)
94
- end
95
-
96
- protected
97
-
98
- def deliver(data, code = 200, format = :json)
99
- status(code)
100
- content_type(format)
101
- data.public_send("to_#{format}")
102
- end
103
-
104
- def show_error(code)
105
- erb :"errors/#{code}", layout: false
106
- end
107
-
108
- end
109
-
110
- module All
111
-
112
- def self.registered(app)
113
- app.register Snails::Database
114
- app.register Snails::Locales
115
- app.register Snails::Assets
116
- end
117
-
118
- end
119
-
120
- module Database
121
-
122
- def self.registered(app)
123
- require 'sinatra/activerecord'
124
-
125
- app.register Sinatra::ActiveRecordExtension
126
-
127
- # app.configure :development do
128
- # ActiveRecord::Base.logger.level = Logger::DEBUG
129
- # end
130
- end
131
-
132
- end
133
-
134
- module Locales
135
-
136
- def self.registered(app)
137
- require 'i18n'
138
- require 'i18n/backend/fallbacks'
139
-
140
- cwd = Pathname.new(Dir.pwd)
141
- app.set :locale, :es
142
- app.set :locales_path, cwd.join('config', 'locales')
143
-
144
- app.helpers do
145
- def t(key); I18n.t(key); end
146
- end
147
-
148
- app.configure do
149
- I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
150
- I18n.load_path = Dir[File.join(app.settings.locales_path, '*.yml')]
151
- I18n.enforce_available_locales = false
152
- I18n.backend.load_translations
153
- end
154
-
155
- app.before do
156
- I18n.locale = app.settings.locale
157
- end
158
- end
159
-
160
- end
161
-
162
- # usage:
163
-
164
- # class App < Snails::App
165
- # register Snails::Assets
166
- #
167
- # # optional:
168
- # set :assets_precompile, %w(js/app.js css/styles.css)
169
- # # also optional, set compressor
170
- # sprockets.css_compressor = :csso
171
- # sprockets.js_compressor = :uglifier
172
- # end
173
-
174
- # Then, in your view:
175
- #
176
- # <script src="/assets/js/app.js"></script>
177
- # <link rel="stylesheet" href="/assets/css/styles.css" />
178
- #
179
-
180
- module Assets
181
-
182
- def self.registered(app)
183
- require 'sprockets-helpers'
184
-
185
- cwd = Pathname.new(Dir.pwd)
186
- app.set :sprockets, Sprockets::Environment.new(cwd)
187
- app.set :assets_prefix, '/assets' # URL
188
- app.set :digest_assets, false
189
- app.set :assets_public_path, -> { cwd.join('public', 'assets') } # output dir
190
- app.set :assets_paths, %w(assets) # source files
191
- app.set :assets_precompile, %w(js/main.js css/main.css)
192
- app.set :assets_remove_digests, false
193
-
194
- app.configure do
195
- app.assets_paths.each do |path|
196
- app.sprockets.append_path cwd.join(path)
197
- end
198
- end
199
-
200
- app.configure :production, :staging do
201
- # app.sprockets.css_compressor = :sass
202
- # app.sprockets.js_compressor = :uglifier
203
- end
204
-
205
- app.configure :development do
206
- # allow asset requests to pass
207
- app.allow_paths.push /^#{app.assets_prefix}(\/\w+)?\/([\w\.-]+)/
208
-
209
- # and serve them
210
- app.get "#{app.assets_prefix}/*" do |path|
211
- env_sprockets = request.env.dup
212
- env_sprockets['PATH_INFO'] = path
213
- app.sprockets.call(env_sprockets)
214
- end
215
- end
216
-
217
- app.helpers do
218
- def asset_path(filename)
219
- file = manifest[filename] or raise "Not found in manifest: #{filename}"
220
- [settings.assets_prefix, file].join('/')
221
- end
222
-
223
- if Snails.env.production?
224
- def manifest
225
- @manifest ||= read_manifest
226
- end
227
- else
228
- def manifest
229
- read_manifest
230
- end
231
- end
232
-
233
- def read_manifest
234
- file = Dir[settings.assets_public_path + '/.*.json'].first or raise "No manifest found at #{path}"
235
- JSON.parse(IO.read(file))['assets']
236
- end
237
- end
238
- end
239
-
240
- module Tasks
241
-
242
- def self.precompile_for(app)
243
- unless app.respond_to?(:assets_public_path)
244
- return puts "#{app.name} doesn't have the Asset module included."
245
- end
246
-
247
- puts "Precompiling #{app.name} assets to #{app.assets_public_path}..."
248
- FileUtils.remove_dir(app.assets_public_path.to_s, true)
249
-
250
- environment = app.sprockets
251
- manifest = ::Sprockets::Manifest.new(environment.index, app.assets_public_path)
252
- manifest.compile(app.assets_precompile)
253
-
254
- if app.assets_remove_digests?
255
- # files = Dir[app.assets_public_path.to_s + '/*/*']
256
- files = `find #{app.assets_public_path}`.split("\n").select { |f| f[/\.(js|css)/] }
257
- remove_digests(files)
258
- end
259
- end
260
-
261
- private
262
-
263
- def self.remove_digests(files)
264
- puts "Removing digests from #{files.length} files..."
265
- files.each do |file|
266
- dir = File.dirname(file)
267
- parts = File.basename(file).split(/-|\./)
268
- if !parts[1] or parts[1].length < 10
269
- # puts "This doesn't look like a digested file: #{file}. Skipping..."
270
- next
271
- end
272
-
273
- dest = File.join(dir, "#{parts.first}.#{parts.last}").sub('.gz', '.' + parts.last(2).join('.'))
274
- FileUtils.mv(file, dest)
275
- puts " --> #{dest}"
276
- end
277
- end
278
- end
279
-
280
- end
281
-
282
- module FormHelpers
283
-
284
- def form_input(object, field, options = {})
285
- id, name, index, label = input_base(object, field, options)
286
- type = (options[:type] || :text).to_sym
287
- value = options[:value] ? "value='#{options[:value]}'" : (type == :password ? '' : "value='#{object.send(field)}'")
288
-
289
- classes = object.errors[field].any? ? 'has-errors' : ''
290
- label + raw_input(index, type, id, name, value, options[:placeholder], classes, options[:required])
291
- end
292
-
293
- def form_password(object, field, options = {})
294
- form_input(object, field, {:type => 'password'}.merge(options))
295
- end
296
-
297
- def form_checkbox(object, field, options = {})
298
- id, name, index, label = input_base(object, field, options)
299
-
300
- type = options[:type] || :checkbox
301
- value = options[:value] ? "value='#{options[:value]}'" : ''
302
-
303
- if type.to_sym == :radio
304
- checked = object.send(field) == options[:value]
305
- value += checked ? " selected='selected'" : ''
306
- else
307
- checked = object.send(field)
308
- value += checked ? " checked='true'" : ''
309
- end
310
-
311
- label + raw_input(index, type, id, name, options[:placeholder], value)
312
- end
313
-
314
- def form_textarea(object, field, options = {})
315
- id, name, index, label = input_base(object, field, options)
316
- style = options[:style] ? "style='#{options[:style]}'" : ''
317
- label + "<textarea #{style} tabindex='#{index}' id='#{id}' name='#{name}'>#{object.send(field)}</textarea>"
318
- end
319
-
320
- def form_select_options(list, selected = nil)
321
- list.map do |name, val|
322
- val = name if val.nil?
323
- sel = val == selected ? 'selected="selected"' : ''
324
- "<option #{sel} value='#{val}'>#{name}</option>"
325
- end.join("\n")
326
- end
327
-
328
- def form_put
329
- '<input type="hidden" name="_method" value="put" />'
330
- end
331
-
332
- def form_submit(text = 'Actualizar', classes = '')
333
- @tabindex = @tabindex ? @tabindex + 1 : 1
334
- "<button tabindex='#{@tabindex}' class='primary #{classes}' type='submit'>#{text}</button>"
335
- end
336
-
337
- def post_button(text, path, opts = {})
338
- form_id = opts.delete(:form_id) || path.gsub(/[^a-z0-9]/, '_').gsub(/_+/, '_')
339
- css_class = opts.delete(:css_class) || ''
340
- input_html = opts.delete(:input_html) || ''
341
- confirm_text = opts.delete(:confirm_text) || 'Seguro?'
342
- submit_val = opts.delete(:value) || text
343
- '<form id="' + form_id + '" style="display:inline" method="post" action="' + url(path) + '" onsubmit="return confirm(\'' + confirm_text + '\');">
344
- ' + input_html + '
345
- <button name="submit" type="submit" class="' + css_class + ' button" value="' + submit_val + '">' + text + '</button>
346
- </form>'
347
- end
348
-
349
- def delete_link(options = {})
350
- post_button(options[:text], options[:path], options.merge({
351
- input_html: "<input type='hidden' name='_method' value='delete' />",
352
- css_class: 'danger'
353
- }))
354
- end
355
-
356
- protected
357
-
358
- def input_base(object, field, options)
359
- label = options[:label] || field.to_s.gsub('_', ' ').capitalize
360
- example = options[:example] ? "<small class='example'>#{options[:example]}</small>" : ''
361
-
362
- id = "#{get_model_name(object).downcase}_#{options[:key] || field}"
363
- name = "#{get_model_name(object).downcase}[#{field}]"
364
- label = options[:label] == false ? '' : "<label for='#{id}'>#{label}#{example}</label>\n"
365
-
366
- @tabindex = @tabindex ? @tabindex + 1 : 1
367
- return id, name, @tabindex, label
368
- end
369
-
370
- def raw_input(index, type, id, name, value, placeholder = '', classes = '', required = false)
371
- req = required ? "required" : ''
372
- "<input #{req} class='#{classes}' tabindex='#{index}' type='#{type}' id='#{id}' name='#{name}' placeholder='#{placeholder}' #{value} />"
373
- end
374
-
375
- def get_model_name(obj)
376
- obj.respond_to?(:field_name) ? obj.field_name : get_class_name(obj.class)
377
- end
378
-
379
- def get_class_name(klass)
380
- klass.model_name.to_s.split("::").last
381
- end
382
-
383
- end
384
-
385
- module SimpleFormat
386
-
387
- def tag(name, options = nil, open = false)
388
- attributes = tag_attributes(options)
389
- "<#{name}#{attributes}#{open ? '>' : ' />'}"
390
- end
391
-
392
- def tag_attributes(options)
393
- return '' unless options
394
- options.inject('') do |all,(key,value)|
395
- next all unless value
396
- all << ' ' if all.empty?
397
- all << %(#{key}="#{value}" )
398
- end.chomp!(' ')
399
- end
400
-
401
- def simple_format(text, options = {})
402
- t = options.delete(:tag) || :p
403
- start_tag = tag(t, options, true)
404
- text = text.to_s.dup
405
- text.gsub!(/\r\n?/, "\n") # \r\n and \r -> \n
406
- text.gsub!(/\n\n+/, "</#{t}>\n\n#{start_tag}") # 2+ newline -> paragraph
407
- text.gsub!(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
408
- text.insert 0, start_tag
409
- text << "</#{t}>"
410
- text
411
- end
412
21
 
22
+ def self.logger
23
+ @logged ||= Logger.new(File.exist?(root.join('log')) ? root.join('log', "#{Snails.env}.log") : nil)
413
24
  end
414
-
415
- module ViewHelpers
416
-
417
- def self.included(base)
418
- Time.include(RelativeTime) unless Time.instance_methods.include?(:relative)
419
- base.include(FormHelpers)
420
- base.include(SimpleFormat)
421
- end
422
-
423
- def action
424
- request.path_info.gsub('/','').blank? ? 'home' : request.path_info.gsub('/',' ')
425
- end
426
-
427
- def partial(name, opts = {})
428
- partial_name = name.to_s["/"] ? name.to_s.reverse.sub("/", "_/").reverse : "_#{name}"
429
- erb(partial_name.to_sym, { layout: false }.merge(opts))
430
- end
431
-
432
- def view(view_name, opts = {})
433
- layout = request.xhr? ? false : true
434
- erb(view_name.to_sym, { layout: layout }.merge(opts))
435
- end
436
-
437
- #########################################
438
- # pagination
439
-
440
- def get_page(counter)
441
- curr = params[:page].to_i
442
- i = (curr == 0 && counter == 1) ? 2
443
- : (curr == 2 && counter == -1) ? 0
444
- : curr + counter
445
- i == 0 ? "" : "/page/#{i}"
446
- end
447
-
448
- def show_pager(array, path)
449
- # remove page from path
450
- path = (env['SCRIPT_NAME'] + path.gsub(/[?|&|\/]page[=|\/]\d+/,''))
451
-
452
- prevlink = '<li>' + link_to("#{path}#{get_page(-1)}", '&larr; Prev').sub('//', '/') + '</li>'
453
- nextlink = array.count != Routes::PER_PAGE ? ""
454
- : '<li>' + link_to("#{path}#{get_page(1)}", 'Next &rarr;').sub('//', '/') + '</li>'
455
-
456
- str = params[:page] ? prevlink + nextlink : nextlink
457
- str != "" ? "<ul class='pager'>" + str + "</ul>" : ''
458
- end
459
-
460
- end
461
-
462
- module RelativeTime
463
-
464
- def in_words
465
- minutes = (((Time.now - self).abs)/60).round
466
- return nil if minutes < 0
467
-
468
- case minutes
469
- when 0..1 then 'menos de un min'
470
- when 2..4 then 'menos de 5 min'
471
- when 5..14 then 'menos de 15 min'
472
- when 15..29 then "media hora"
473
- when 30..59 then "#{minutes} minutos"
474
- when 60..119 then '1 hora'
475
- when 120..239 then '2 horas'
476
- when 240..479 then '4 horas'
477
- when 480..719 then '8 horas'
478
- when 720..1439 then '12 horas'
479
- when 1440..11519 then "#{(minutes/1440).floor} días"
480
- when 11520..43199 then "#{(minutes/11520).floor} semanas"
481
- when 43200..525599 then "#{(minutes/43200).floor} meses"
482
- else "#{(minutes/525600).floor} años"
483
- end
484
- end
485
-
486
- def relative
487
- if str = in_words
488
- if Time.now < self
489
- # "#{str} más"
490
- "en #{str}"
491
- else
492
- "hace #{str}"
493
- end
494
- end
495
- end
496
-
497
- end
498
-
499
- end
25
+ end
@@ -0,0 +1,481 @@
1
+ %w(
2
+ snails
3
+ active_support/string_inquirer
4
+ active_support/core_ext/hash
5
+ sinatra/base
6
+ sinatra/content_for
7
+ sinatra/flash
8
+ ).each { |lib| require lib }
9
+
10
+ module Snails
11
+
12
+ module RequiredParams
13
+ def requires!(req, hash = params)
14
+ if req.is_a?(Hash)
15
+ req.each do |k, vals|
16
+ if vals.is_a?(Array) or vals.is_a?(Hash)
17
+ halt(400, "Missing: #{k} in #{hash}") if hash[k].nil?
18
+ requires!(vals, hash[k])
19
+ else
20
+ requires!(k, hash)
21
+ end
22
+ end
23
+ elsif req.nil? or (req.is_a?(Symbol) and hash[req].nil?) \
24
+ or (req.is_a?(Array) and req.any? { |p| hash[p].nil? })
25
+ halt(400, "Required parameters: #{req} (in #{hash})")
26
+ end
27
+ end
28
+ end
29
+
30
+ class App < Sinatra::Base
31
+
32
+ def self.inherited(base)
33
+ Snails.apps << base
34
+ super
35
+ end
36
+
37
+ set :protection, except: :frame_options
38
+ set :views, Snails.root.join('lib', 'views')
39
+ set :session_secret, ENV.fetch('SESSION_SECRET') { SecureRandom.hex }
40
+ set :static_paths, %w(/css /img /js /files /fonts favicon.ico)
41
+
42
+ enable :sessions
43
+ enable :method_override
44
+ enable :logging
45
+
46
+ register Sinatra::Flash
47
+ use Rack::CommonLogger, Snails.logger
48
+ use Rack::Static, urls: static_paths, root: 'public'
49
+
50
+ configure :production, :staging do
51
+ set :raise_errors, true
52
+ set :dump_errors, false
53
+ end
54
+
55
+ configure :development do
56
+ set :raise_errors, true
57
+ set :show_exceptions, true
58
+ set :log_level, Logger::DEBUG
59
+ end
60
+
61
+ helpers do
62
+ include RequiredParams
63
+ include Sinatra::ContentFor
64
+ def logger; Snails.logger; end
65
+ end
66
+
67
+ error do
68
+ err = request.env['sinatra.error']
69
+ logger.error err.message
70
+ logger.error err.backtrace.first(3).join("\n")
71
+ halt(500, err.message)
72
+ end
73
+
74
+ not_found do
75
+ show_error(404)
76
+ end
77
+
78
+ protected
79
+
80
+ def deliver(data, code = 200, format = :json)
81
+ status(code)
82
+ content_type(format)
83
+ data.public_send("to_#{format}")
84
+ end
85
+
86
+ def show_error(code)
87
+ erb :"errors/#{code}", layout: false
88
+ end
89
+
90
+ end
91
+
92
+ module All
93
+
94
+ def self.registered(app)
95
+ app.register Snails::Database
96
+ app.register Snails::Locales
97
+ app.register Snails::Assets
98
+ end
99
+
100
+ end
101
+
102
+ module Database
103
+
104
+ def self.registered(app)
105
+ require 'sinatra/activerecord'
106
+
107
+ app.register Sinatra::ActiveRecordExtension
108
+
109
+ # app.configure :development do
110
+ # ActiveRecord::Base.logger.level = Logger::DEBUG
111
+ # end
112
+ end
113
+
114
+ end
115
+
116
+ module Locales
117
+
118
+ def self.registered(app)
119
+ require 'i18n'
120
+ require 'i18n/backend/fallbacks'
121
+
122
+ cwd = Pathname.new(Dir.pwd)
123
+ app.set :locale, :es
124
+ app.set :locales_path, cwd.join('config', 'locales')
125
+
126
+ app.helpers do
127
+ def t(key); I18n.t(key); end
128
+ end
129
+
130
+ app.configure do
131
+ I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
132
+ I18n.load_path = Dir[File.join(app.settings.locales_path, '*.yml')]
133
+ I18n.enforce_available_locales = false
134
+ I18n.backend.load_translations
135
+ end
136
+
137
+ app.before do
138
+ I18n.locale = app.settings.locale
139
+ end
140
+ end
141
+
142
+ end
143
+
144
+ # usage:
145
+
146
+ # class App < Snails::App
147
+ # register Snails::Assets
148
+ #
149
+ # # optional:
150
+ # set :assets_precompile, %w(js/app.js css/styles.css)
151
+ # # also optional, set compressor
152
+ # sprockets.css_compressor = :csso
153
+ # sprockets.js_compressor = :uglifier
154
+ # end
155
+
156
+ # Then, in your view:
157
+ #
158
+ # <script src="/assets/js/app.js"></script>
159
+ # <link rel="stylesheet" href="/assets/css/styles.css" />
160
+ #
161
+
162
+ module Assets
163
+
164
+ def self.registered(app)
165
+ require 'sprockets-helpers'
166
+
167
+ cwd = Pathname.new(Dir.pwd)
168
+ app.set :sprockets, Sprockets::Environment.new(cwd)
169
+ app.set :assets_prefix, '/assets' # URL
170
+ app.set :digest_assets, false
171
+ app.set :assets_public_path, -> { cwd.join('public', 'assets') } # output dir
172
+ app.set :assets_paths, %w(assets) # source files
173
+ app.set :assets_precompile, %w(js/main.js css/main.css)
174
+ app.set :assets_remove_digests, false
175
+
176
+ app.configure do
177
+ app.assets_paths.each do |path|
178
+ app.sprockets.append_path cwd.join(path)
179
+ end
180
+ end
181
+
182
+ app.configure :production, :staging do
183
+ # app.sprockets.css_compressor = :sass
184
+ # app.sprockets.js_compressor = :uglifier
185
+ end
186
+
187
+ app.configure :development do
188
+ # allow asset requests to pass
189
+ app.allow_paths.push /^#{app.assets_prefix}(\/\w+)?\/([\w\.-]+)/
190
+
191
+ # and serve them
192
+ app.get "#{app.assets_prefix}/*" do |path|
193
+ env_sprockets = request.env.dup
194
+ env_sprockets['PATH_INFO'] = path
195
+ app.sprockets.call(env_sprockets)
196
+ end
197
+ end
198
+
199
+ app.helpers do
200
+ def asset_path(filename)
201
+ file = manifest[filename] or raise "Not found in manifest: #{filename}"
202
+ [settings.assets_prefix, file].join('/')
203
+ end
204
+
205
+ if Snails.env.production?
206
+ def manifest
207
+ @manifest ||= read_manifest
208
+ end
209
+ else
210
+ def manifest
211
+ read_manifest
212
+ end
213
+ end
214
+
215
+ def read_manifest
216
+ file = Dir[settings.assets_public_path + '/.*.json'].first or raise "No manifest found at #{path}"
217
+ JSON.parse(IO.read(file))['assets']
218
+ end
219
+ end
220
+ end
221
+
222
+ module Tasks
223
+
224
+ def self.precompile_for(app)
225
+ unless app.respond_to?(:assets_public_path)
226
+ return puts "#{app.name} doesn't have the Asset module included."
227
+ end
228
+
229
+ puts "Precompiling #{app.name} assets to #{app.assets_public_path}..."
230
+ FileUtils.remove_dir(app.assets_public_path.to_s, true)
231
+
232
+ environment = app.sprockets
233
+ manifest = ::Sprockets::Manifest.new(environment.index, app.assets_public_path)
234
+ manifest.compile(app.assets_precompile)
235
+
236
+ if app.assets_remove_digests?
237
+ # files = Dir[app.assets_public_path.to_s + '/*/*']
238
+ files = `find #{app.assets_public_path}`.split("\n").select { |f| f[/\.(js|css)/] }
239
+ remove_digests(files)
240
+ end
241
+ end
242
+
243
+ private
244
+
245
+ def self.remove_digests(files)
246
+ puts "Removing digests from #{files.length} files..."
247
+ files.each do |file|
248
+ dir = File.dirname(file)
249
+ parts = File.basename(file).split(/-|\./)
250
+ if !parts[1] or parts[1].length < 10
251
+ # puts "This doesn't look like a digested file: #{file}. Skipping..."
252
+ next
253
+ end
254
+
255
+ dest = File.join(dir, "#{parts.first}.#{parts.last}").sub('.gz', '.' + parts.last(2).join('.'))
256
+ FileUtils.mv(file, dest)
257
+ puts " --> #{dest}"
258
+ end
259
+ end
260
+ end
261
+
262
+ end
263
+
264
+ module FormHelpers
265
+
266
+ def form_input(object, field, options = {})
267
+ id, name, index, label = input_base(object, field, options)
268
+ type = (options[:type] || :text).to_sym
269
+ value = options[:value] ? "value='#{options[:value]}'" : (type == :password ? '' : "value='#{object.send(field)}'")
270
+
271
+ classes = object.errors[field].any? ? 'has-errors' : ''
272
+ label + raw_input(index, type, id, name, value, options[:placeholder], classes, options[:required])
273
+ end
274
+
275
+ def form_password(object, field, options = {})
276
+ form_input(object, field, {:type => 'password'}.merge(options))
277
+ end
278
+
279
+ def form_checkbox(object, field, options = {})
280
+ id, name, index, label = input_base(object, field, options)
281
+
282
+ type = options[:type] || :checkbox
283
+ value = options[:value] ? "value='#{options[:value]}'" : ''
284
+
285
+ if type.to_sym == :radio
286
+ checked = object.send(field) == options[:value]
287
+ value += checked ? " selected='selected'" : ''
288
+ else
289
+ checked = object.send(field)
290
+ value += checked ? " checked='true'" : ''
291
+ end
292
+
293
+ label + raw_input(index, type, id, name, options[:placeholder], value)
294
+ end
295
+
296
+ def form_textarea(object, field, options = {})
297
+ id, name, index, label = input_base(object, field, options)
298
+ style = options[:style] ? "style='#{options[:style]}'" : ''
299
+ label + "<textarea #{style} tabindex='#{index}' id='#{id}' name='#{name}'>#{object.send(field)}</textarea>"
300
+ end
301
+
302
+ def form_select_options(list, selected = nil)
303
+ list.map do |name, val|
304
+ val = name if val.nil?
305
+ sel = val == selected ? 'selected="selected"' : ''
306
+ "<option #{sel} value='#{val}'>#{name}</option>"
307
+ end.join("\n")
308
+ end
309
+
310
+ def form_put
311
+ '<input type="hidden" name="_method" value="put" />'
312
+ end
313
+
314
+ def form_submit(text = 'Actualizar', classes = '')
315
+ @tabindex = @tabindex ? @tabindex + 1 : 1
316
+ "<button tabindex='#{@tabindex}' class='primary #{classes}' type='submit'>#{text}</button>"
317
+ end
318
+
319
+ def post_button(text, path, opts = {})
320
+ form_id = opts.delete(:form_id) || path.gsub(/[^a-z0-9]/, '_').gsub(/_+/, '_')
321
+ css_class = opts.delete(:css_class) || ''
322
+ input_html = opts.delete(:input_html) || ''
323
+ confirm_text = opts.delete(:confirm_text) || 'Seguro?'
324
+ submit_val = opts.delete(:value) || text
325
+ '<form id="' + form_id + '" style="display:inline" method="post" action="' + url(path) + '" onsubmit="return confirm(\'' + confirm_text + '\');">
326
+ ' + input_html + '
327
+ <button name="submit" type="submit" class="' + css_class + ' button" value="' + submit_val + '">' + text + '</button>
328
+ </form>'
329
+ end
330
+
331
+ def delete_link(options = {})
332
+ post_button(options[:text], options[:path], options.merge({
333
+ input_html: "<input type='hidden' name='_method' value='delete' />",
334
+ css_class: 'danger'
335
+ }))
336
+ end
337
+
338
+ protected
339
+
340
+ def input_base(object, field, options)
341
+ label = options[:label] || field.to_s.gsub('_', ' ').capitalize
342
+ example = options[:example] ? "<small class='example'>#{options[:example]}</small>" : ''
343
+
344
+ id = "#{get_model_name(object).downcase}_#{options[:key] || field}"
345
+ name = "#{get_model_name(object).downcase}[#{field}]"
346
+ label = options[:label] == false ? '' : "<label for='#{id}'>#{label}#{example}</label>\n"
347
+
348
+ @tabindex = @tabindex ? @tabindex + 1 : 1
349
+ return id, name, @tabindex, label
350
+ end
351
+
352
+ def raw_input(index, type, id, name, value, placeholder = '', classes = '', required = false)
353
+ req = required ? "required" : ''
354
+ "<input #{req} class='#{classes}' tabindex='#{index}' type='#{type}' id='#{id}' name='#{name}' placeholder='#{placeholder}' #{value} />"
355
+ end
356
+
357
+ def get_model_name(obj)
358
+ obj.respond_to?(:field_name) ? obj.field_name : get_class_name(obj.class)
359
+ end
360
+
361
+ def get_class_name(klass)
362
+ klass.model_name.to_s.split("::").last
363
+ end
364
+
365
+ end
366
+
367
+ module SimpleFormat
368
+
369
+ def tag(name, options = nil, open = false)
370
+ attributes = tag_attributes(options)
371
+ "<#{name}#{attributes}#{open ? '>' : ' />'}"
372
+ end
373
+
374
+ def tag_attributes(options)
375
+ return '' unless options
376
+ options.inject('') do |all,(key,value)|
377
+ next all unless value
378
+ all << ' ' if all.empty?
379
+ all << %(#{key}="#{value}" )
380
+ end.chomp!(' ')
381
+ end
382
+
383
+ def simple_format(text, options = {})
384
+ t = options.delete(:tag) || :p
385
+ start_tag = tag(t, options, true)
386
+ text = text.to_s.dup
387
+ text.gsub!(/\r\n?/, "\n") # \r\n and \r -> \n
388
+ text.gsub!(/\n\n+/, "</#{t}>\n\n#{start_tag}") # 2+ newline -> paragraph
389
+ text.gsub!(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
390
+ text.insert 0, start_tag
391
+ text << "</#{t}>"
392
+ text
393
+ end
394
+
395
+ end
396
+
397
+ module ViewHelpers
398
+
399
+ def self.included(base)
400
+ Time.include(RelativeTime) unless Time.instance_methods.include?(:relative)
401
+ base.include(FormHelpers)
402
+ base.include(SimpleFormat)
403
+ end
404
+
405
+ def action
406
+ request.path_info.gsub('/','').blank? ? 'home' : request.path_info.gsub('/',' ')
407
+ end
408
+
409
+ def partial(name, opts = {})
410
+ partial_name = name.to_s["/"] ? name.to_s.reverse.sub("/", "_/").reverse : "_#{name}"
411
+ erb(partial_name.to_sym, { layout: false }.merge(opts))
412
+ end
413
+
414
+ def view(view_name, opts = {})
415
+ layout = request.xhr? ? false : true
416
+ erb(view_name.to_sym, { layout: layout }.merge(opts))
417
+ end
418
+
419
+ #########################################
420
+ # pagination
421
+
422
+ def get_page(counter)
423
+ curr = params[:page].to_i
424
+ i = (curr == 0 && counter == 1) ? 2
425
+ : (curr == 2 && counter == -1) ? 0
426
+ : curr + counter
427
+ i == 0 ? "" : "/page/#{i}"
428
+ end
429
+
430
+ def show_pager(array, path)
431
+ # remove page from path
432
+ path = (env['SCRIPT_NAME'] + path.gsub(/[?|&|\/]page[=|\/]\d+/,''))
433
+
434
+ prevlink = '<li>' + link_to("#{path}#{get_page(-1)}", '&larr; Prev').sub('//', '/') + '</li>'
435
+ nextlink = array.count != Routes::PER_PAGE ? ""
436
+ : '<li>' + link_to("#{path}#{get_page(1)}", 'Next &rarr;').sub('//', '/') + '</li>'
437
+
438
+ str = params[:page] ? prevlink + nextlink : nextlink
439
+ str != "" ? "<ul class='pager'>" + str + "</ul>" : ''
440
+ end
441
+
442
+ end
443
+
444
+ module RelativeTime
445
+
446
+ def in_words
447
+ minutes = (((Time.now - self).abs)/60).round
448
+ return nil if minutes < 0
449
+
450
+ case minutes
451
+ when 0..1 then 'menos de un min'
452
+ when 2..4 then 'menos de 5 min'
453
+ when 5..14 then 'menos de 15 min'
454
+ when 15..29 then "media hora"
455
+ when 30..59 then "#{minutes} minutos"
456
+ when 60..119 then '1 hora'
457
+ when 120..239 then '2 horas'
458
+ when 240..479 then '4 horas'
459
+ when 480..719 then '8 horas'
460
+ when 720..1439 then '12 horas'
461
+ when 1440..11519 then "#{(minutes/1440).floor} días"
462
+ when 11520..43199 then "#{(minutes/11520).floor} semanas"
463
+ when 43200..525599 then "#{(minutes/43200).floor} meses"
464
+ else "#{(minutes/525600).floor} años"
465
+ end
466
+ end
467
+
468
+ def relative
469
+ if str = in_words
470
+ if Time.now < self
471
+ # "#{str} más"
472
+ "en #{str}"
473
+ else
474
+ "hace #{str}"
475
+ end
476
+ end
477
+ end
478
+
479
+ end
480
+
481
+ end
@@ -12,7 +12,6 @@ module Snails
12
12
  @queue = :emails
13
13
 
14
14
  def initialize(opts)
15
- cwd = Pathname.new(Dir.pwd)
16
15
  mail_config = (opts[:smtp] || opts[:mail]) or raise ":smtp options missing"
17
16
 
18
17
  if key = mail_config.dig(:dkim, :private_key) and File.exist?(key)
@@ -23,11 +22,10 @@ module Snails
23
22
  end
24
23
 
25
24
  Tuktuk.options = mail_config
26
-
27
25
  @from_email = opts[:from] or raise ":from required"
28
26
  @base_subject = opts[:base_subject] || ''
29
- @views = opts[:views] || cwd.join('lib', 'views')
30
- @logfile = opts[:logfile] || cwd.join('log', 'mailer.log')
27
+ @views = opts[:views] || Snails.root.join('lib', 'views')
28
+ @logfile = opts[:logfile] # || Snails.root.join('log', 'mailer.log')
31
29
  end
32
30
 
33
31
  def email(name, &block)
@@ -92,7 +90,7 @@ A <%= @exception.class %> occurred in <%= @url %>:
92
90
  end
93
91
 
94
92
  def logger
95
- @logger ||= Logger.new(@logfile)
93
+ @logger ||= @logfile ? Logger.new(@logfile) : Snails.logger
96
94
  end
97
95
 
98
96
  def send_email(from: nil, to:, subject:, body: nil, template: nil, html_body: nil, html_template: nil)
@@ -3,7 +3,7 @@
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "snails"
6
- s.version = '0.0.8'
6
+ s.version = '0.1.1'
7
7
  s.platform = Gem::Platform::RUBY
8
8
  s.authors = ['Tomás Pollak']
9
9
  s.email = ['tomas@forkhq.com']
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: snails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tomás Pollak
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-11-07 00:00:00.000000000 Z
11
+ date: 2018-11-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -141,6 +141,7 @@ files:
141
141
  - example/Rakefile
142
142
  - example/config.ru
143
143
  - lib/snails.rb
144
+ - lib/snails/app.rb
144
145
  - lib/snails/mailer.rb
145
146
  - lib/snails/tasks.rb
146
147
  - snails.gemspec