snails 0.0.8 → 0.1.1

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 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