%w( snails active_support/string_inquirer active_support/core_ext/hash sinatra/base sinatra/content_for sinatra/flash ).each { |lib| require lib } module Snails module RequiredParams def requires!(req, hash = params) if req.is_a?(Hash) req.each do |k, vals| if vals.is_a?(Array) or vals.is_a?(Hash) halt(400, "Missing: #{k} in #{hash}") if hash[k].nil? requires!(vals, hash[k]) else requires!(k, hash) end end elsif req.nil? or (req.is_a?(Symbol) and hash[req].nil?) \ or (req.is_a?(Array) and req.any? { |p| hash[p].nil? }) halt(400, "Required parameters: #{req} (in #{hash})") end end end class App < Sinatra::Base def self.inherited(base) Snails.apps << base super end set :protection, except: :frame_options set :views, Snails.root.join('lib', 'views') set :session_secret, ENV.fetch('SESSION_SECRET') { SecureRandom.hex } set :static_paths, %w(/css /img /js /files /fonts favicon.ico) enable :sessions enable :method_override enable :logging register Sinatra::Flash use Rack::CommonLogger, Snails.logger use Rack::Static, urls: static_paths, root: 'public' configure :production, :staging do set :raise_errors, true set :dump_errors, false end configure :development do set :raise_errors, true set :show_exceptions, true set :log_level, Logger::DEBUG end helpers do include RequiredParams include Sinatra::ContentFor def logger; Snails.logger; end end error do err = request.env['sinatra.error'] logger.error err.message logger.error err.backtrace.first(3).join("\n") halt(500, err.message) end not_found do show_error(404) end protected def deliver(data, code = 200, format = :json) status(code) content_type(format) data.public_send("to_#{format}") end def show_error(code) erb :"errors/#{code}", layout: false end end module All def self.registered(app) app.register Snails::Database app.register Snails::Locales app.register Snails::Assets end end module Database def self.registered(app) require 'sinatra/activerecord' app.register Sinatra::ActiveRecordExtension # app.configure :development do # ActiveRecord::Base.logger.level = Logger::DEBUG # end end end module Locales def self.registered(app) require 'i18n' require 'i18n/backend/fallbacks' cwd = Pathname.new(Dir.pwd) app.set :locale, :es app.set :locales_path, cwd.join('config', 'locales') app.helpers do def t(key); I18n.t(key); end end app.configure do I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks) I18n.load_path = Dir[File.join(app.settings.locales_path, '*.yml')] I18n.enforce_available_locales = false I18n.backend.load_translations end app.before do I18n.locale = app.settings.locale end end end # usage: # class App < Snails::App # register Snails::Assets # # # optional: # set :assets_precompile, %w(js/app.js css/styles.css) # # also optional, set compressor # sprockets.css_compressor = :csso # sprockets.js_compressor = :uglifier # end # Then, in your view: # # # # module Assets def self.registered(app) require 'sprockets-helpers' cwd = Pathname.new(Dir.pwd) app.set :sprockets, Sprockets::Environment.new(cwd) app.set :assets_prefix, '/assets' # URL app.set :digest_assets, false app.set :assets_public_path, -> { cwd.join('public', 'assets') } # output dir app.set :assets_paths, %w(assets) # source files app.set :assets_precompile, %w(js/main.js css/main.css) app.set :assets_remove_digests, false app.configure do app.assets_paths.each do |path| app.sprockets.append_path cwd.join(path) end end app.configure :production, :staging do # app.sprockets.css_compressor = :sass # app.sprockets.js_compressor = :uglifier end app.configure :development do # allow asset requests to pass app.allow_paths.push /^#{app.assets_prefix}(\/\w+)?\/([\w\.-]+)/ # and serve them app.get "#{app.assets_prefix}/*" do |path| env_sprockets = request.env.dup env_sprockets['PATH_INFO'] = path app.sprockets.call(env_sprockets) end end app.helpers do def asset_path(filename) file = manifest[filename] or raise "Not found in manifest: #{filename}" [settings.assets_prefix, file].join('/') end if Snails.env.production? def manifest @manifest ||= read_manifest end else def manifest read_manifest end end def read_manifest file = Dir[settings.assets_public_path + '/.*.json'].first or raise "No manifest found at #{path}" JSON.parse(IO.read(file))['assets'] end end end module Tasks def self.precompile_for(app) unless app.respond_to?(:assets_public_path) return puts "#{app.name} doesn't have the Asset module included." end puts "Precompiling #{app.name} assets to #{app.assets_public_path}..." FileUtils.remove_dir(app.assets_public_path.to_s, true) environment = app.sprockets manifest = ::Sprockets::Manifest.new(environment.index, app.assets_public_path) manifest.compile(app.assets_precompile) if app.assets_remove_digests? # files = Dir[app.assets_public_path.to_s + '/*/*'] files = `find #{app.assets_public_path}`.split("\n").select { |f| f[/\.(js|css)/] } remove_digests(files) end end private def self.remove_digests(files) puts "Removing digests from #{files.length} files..." files.each do |file| dir = File.dirname(file) parts = File.basename(file).split(/-|\./) if !parts[1] or parts[1].length < 10 # puts "This doesn't look like a digested file: #{file}. Skipping..." next end dest = File.join(dir, "#{parts.first}.#{parts.last}").sub('.gz', '.' + parts.last(2).join('.')) FileUtils.mv(file, dest) puts " --> #{dest}" end end end end module FormHelpers def form_input(object, field, options = {}) id, name, index, label = input_base(object, field, options) type = (options[:type] || :text).to_sym value = options[:value] ? "value='#{options[:value]}'" : (type == :password ? '' : "value='#{object.send(field)}'") classes = object.errors[field].any? ? 'has-errors' : '' label + raw_input(index, type, id, name, value, options[:placeholder], classes, options[:required]) end def form_password(object, field, options = {}) form_input(object, field, {:type => 'password'}.merge(options)) end def form_checkbox(object, field, options = {}) id, name, index, label = input_base(object, field, options) type = options[:type] || :checkbox value = options[:value] ? "value='#{options[:value]}'" : '' if type.to_sym == :radio checked = object.send(field) == options[:value] value += checked ? " selected='selected'" : '' else checked = object.send(field) value += checked ? " checked='true'" : '' end label + raw_input(index, type, id, name, options[:placeholder], value) end def form_textarea(object, field, options = {}) id, name, index, label = input_base(object, field, options) style = options[:style] ? "style='#{options[:style]}'" : '' label + "" end def form_select_options(list, selected = nil) list.map do |name, val| val = name if val.nil? sel = val == selected ? 'selected="selected"' : '' "" end.join("\n") end def form_put '' end def form_submit(text = 'Actualizar', classes = '') @tabindex = @tabindex ? @tabindex + 1 : 1 "" end def post_button(text, path, opts = {}) form_id = opts.delete(:form_id) || path.gsub(/[^a-z0-9]/, '_').gsub(/_+/, '_') css_class = opts.delete(:css_class) || '' input_html = opts.delete(:input_html) || '' confirm_text = opts.delete(:confirm_text) || 'Seguro?' submit_val = opts.delete(:value) || text '
' + input_html + '
' end def delete_link(options = {}) post_button(options[:text], options[:path], options.merge({ input_html: "", css_class: 'danger' })) end protected def input_base(object, field, options) label = options[:label] || field.to_s.gsub('_', ' ').capitalize example = options[:example] ? "#{options[:example]}" : '' id = "#{get_model_name(object).downcase}_#{options[:key] || field}" name = "#{get_model_name(object).downcase}[#{field}]" label = options[:label] == false ? '' : "\n" @tabindex = @tabindex ? @tabindex + 1 : 1 return id, name, @tabindex, label end def raw_input(index, type, id, name, value, placeholder = '', classes = '', required = false) req = required ? "required" : '' "" end def get_model_name(obj) obj.respond_to?(:field_name) ? obj.field_name : get_class_name(obj.class) end def get_class_name(klass) klass.model_name.to_s.split("::").last end end module SimpleFormat def tag(name, options = nil, open = false) attributes = tag_attributes(options) "<#{name}#{attributes}#{open ? '>' : ' />'}" end def tag_attributes(options) return '' unless options options.inject('') do |all,(key,value)| next all unless value all << ' ' if all.empty? all << %(#{key}="#{value}" ) end.chomp!(' ') end def simple_format(text, options = {}) t = options.delete(:tag) || :p start_tag = tag(t, options, true) text = text.to_s.dup text.gsub!(/\r\n?/, "\n") # \r\n and \r -> \n text.gsub!(/\n\n+/, "\n\n#{start_tag}") # 2+ newline -> paragraph text.gsub!(/([^\n]\n)(?=[^\n])/, '\1
') # 1 newline -> br text.insert 0, start_tag text << "" text end end module ViewHelpers def self.included(base) Time.include(RelativeTime) unless Time.instance_methods.include?(:relative) base.include(FormHelpers) base.include(SimpleFormat) end def action request.path_info.gsub('/','').blank? ? 'home' : request.path_info.gsub('/',' ') end def partial(name, opts = {}) partial_name = name.to_s["/"] ? name.to_s.reverse.sub("/", "_/").reverse : "_#{name}" erb(partial_name.to_sym, { layout: false }.merge(opts)) end def view(view_name, opts = {}) layout = request.xhr? ? false : true erb(view_name.to_sym, { layout: layout }.merge(opts)) end ######################################### # pagination def get_page(counter) curr = params[:page].to_i i = (curr == 0 && counter == 1) ? 2 : (curr == 2 && counter == -1) ? 0 : curr + counter i == 0 ? "" : "/page/#{i}" end def show_pager(array, path) # remove page from path path = (env['SCRIPT_NAME'] + path.gsub(/[?|&|\/]page[=|\/]\d+/,'')) prevlink = '
  • ' + link_to("#{path}#{get_page(-1)}", '← Prev').sub('//', '/') + '
  • ' nextlink = array.count != Routes::PER_PAGE ? "" : '
  • ' + link_to("#{path}#{get_page(1)}", 'Next →').sub('//', '/') + '
  • ' str = params[:page] ? prevlink + nextlink : nextlink str != "" ? "" : '' end end module RelativeTime def in_words minutes = (((Time.now - self).abs)/60).round return nil if minutes < 0 case minutes when 0..1 then 'menos de un min' when 2..4 then 'menos de 5 min' when 5..14 then 'menos de 15 min' when 15..29 then "media hora" when 30..59 then "#{minutes} minutos" when 60..119 then '1 hora' when 120..239 then '2 horas' when 240..479 then '4 horas' when 480..719 then '8 horas' when 720..1439 then '12 horas' when 1440..11519 then "#{(minutes/1440).floor} días" when 11520..43199 then "#{(minutes/11520).floor} semanas" when 43200..525599 then "#{(minutes/43200).floor} meses" else "#{(minutes/525600).floor} años" end end def relative if str = in_words if Time.now < self # "#{str} más" "en #{str}" else "hace #{str}" end end end end end