GordonDiggs/cataloguais

View on GitHub
cataloguais.rb

Summary

Maintainability
A
1 hr
Test Coverage
#!/usr/bin/env ruby
# encoding: utf-8
require 'rubygems'
require 'bundler'
require 'uri'
require 'csv'

Bundler.require
require "sinatra/config_file"
require_relative "extensions/string"

enable :sessions

configure do
  config_file 'settings.yml'

  # require the model(s) before setting up the database
  require_relative "models/item"
  DataMapper.finalize

  # set the input width based on the number of fields
  set :item_width, 840 / settings.fields.count

  # robotize the sort order
  set :sort_order, settings.sort_order.collect {|sort| sort.robotize}

  # initialize the graph urls on startup
  set :graph_urls, {}

  set :cache, Dalli::Client.new

end

configure :production do
  DataMapper::Logger.new($stdout, :info)
end

configure :test do
  ENV['DATABASE_URL'] = 'postgres://localhost/cataloguais_test'
  DataMapper::Logger.new($stdout, :error)
end

configure :development do
  ENV['ADMIN_PASSWORD'] = 'test'
  ENV['DATABASE_URL'] = 'postgres://localhost/cataloguais'
  DataMapper::Logger.new($stdout, :debug)
end

configure do
  DataMapper.setup(:default, ENV['DATABASE_URL'])
  DataMapper.auto_upgrade!
end

before do
  if ENV['ADMIN_PASSWORD'].nil?
    warn "!!! Admin password not set - editing will be enabled by default" unless ENV['RACK_ENV'] == 'test'
    session['editing_enabled'] = true
  end
end

before /(new|update|delete|import)/ do
  if !session['editing_enabled']
    data = { :status => 'error', :message => 'Hey, Mike! Editing must be enabled to do that!' }.to_json
    halt data
  end
end

# empty graph urls so they will be recreated the next time
# the graphs page is visited
after /(new|update|delete|import)/ do
  settings.graph_urls = {}
end

get '/' do
  @fields = settings.fields + ['Added On']
  @sort = if params[:sort]
            params[:sort].gsub!(/added_on/, 'created_at')
            [params[:sort]] + (settings.sort_order - [params[:sort]])
          else
            settings.sort_order
          end
  @direction = params[:direction] || :asc
  @direction = @direction.to_sym if @direction

  # try to fetch results from cache
  cache_key = get_cache_key [@sort, @direction, params[:search]]
  @items = settings.cache.get(cache_key)
  if !@items
    @items = Item.search_and_sort(@sort.dup, @direction, params[:search])
    settings.cache.set(cache_key, @items)
  end

  haml :index
end

get '/export' do
  headers "Content-Disposition" => "attachment;filename=collection_#{Time.now.strftime("%y%m%d%H%M%S")}.csv", "Content-Type" => "application/octet-stream"
  CSV.generate do |file|
    file << Item.fields
    Item.all.each do |item|
      file << item.to_a
    end
  end
end

get '/random' do
  @items = [Item.get_random]
  @sort = settings.sort_order
  @direction = :asc
  haml :index
end

get '/occurrences' do
  if params['field']
    { :occurrence => get_occurrence(params['field']) }.to_json
  else
    { :occurrence => get_occurrences }.to_json
  end
end

post '/new' do
  item = Item.create(params[:item])
  { :status => 'success', :message => 'Item successfully added.', :item_markup => item_table_row(item) }.to_json
end

post '/update/:id' do
  params[:item].delete('added_on')
  item = Item.first(:id => params[:id])
  item.attributes = params[:item]
  item.save!
  { :status => 'success', :message => 'Item successfully updated.', :item_markup => item_table_row(item) }.to_json
end

delete '/delete/:id' do
  Item.first(:id => params[:id]).destroy
  {:status => 'success', :message => 'Item successfully deleted.'}.to_json
end

post '/import' do
  redirect('/?message=You must upload a file') if !params[:file]

  # construct an array of hashes from the data
  # from http://snippets.dzone.com/posts/show/3899
  csv_data = CSV.read params[:file][:tempfile]
  headers = csv_data.shift.map {|i| i.to_s }
  string_data = csv_data.map {|row| row.map {|cell| cell.to_s.force_encoding('utf-8') } }
  array_of_hashes = string_data.map {|row| Hash[*headers.zip(row).flatten] }

  Item.transaction do
    array_of_hashes.each { |attrs| Item.create(attrs) }
  end

  redirect "/?message=Imported #{array_of_hashes.count} items"
end

get '/:stylesheet.css' do
  sass params[:stylesheet].to_sym
end

post '/login' do
  session['editing_enabled'] = true if ENV['ADMIN_PASSWORD'] && params[:password] == ENV['ADMIN_PASSWORD']
  redirect '/'
end

get '/logout' do
  session['editing_enabled'] = nil
  redirect '/'
end

get '/graphs' do
  set_graph_urls if settings.graph_urls.empty?
  haml :graphs
end

# catch trailing spaces from https://gist.github.com/867165
get %r{(.+)/$} do |r| redirect r; end;

def get_cache_key(arr)
  "#{Digest::MD5.hexdigest(arr.join('::'))}_#{Item.cache_key}"
end

# render the row of the table for a given partial
def item_table_row(item)
  @fields ||= settings.fields + ['Added On']
  @item = item
  haml :_item, :layout => false
end

def opposite_direction
  @direction == :asc ? :desc : :asc
end

# Get the occurrences of values for each field on Item
def get_occurrences
  occurrences = {}

  settings.fields.each do |field|
    occurrences[field] = get_occurrence(field, false)
  end

  occurrences
end

def get_occurrence(field, as_array = true)
  data = as_array ? [[field.to_s, 'num']] : {}
  others = 0

  # use SQL grouping for fast calculation
  items = DataMapper.repository.adapter.select("SELECT #{field.robotize} as \"col\", COUNT(*) as \"times\" FROM items GROUP BY #{field.robotize} ORDER BY \"times\" desc")

  # copy the items into the occurrences (since they are an array of structs after selection)
  items.each do |item|
    if as_array
      if item[:times] == 1
        others += 1
      else
        data << [item[:col], item[:times]]
      end
    else
      data[item[:col]] = item[:times]
    end
  end

  # group all the 1s together
  if !as_array && (others = data.select{|k,v| v==1}.size) > 1
    data.delete_if{|k,v| v==1}
    data["Other"] = others
  end

  data << ['Other', others] if as_array

  data
end

# calculate the occurrences and set the graph urls
def set_graph_urls
  get_occurrences.each do |label, data_set|
    # generate labels with the number of items
    labels = data_set.keys.each_with_index.collect { |key, i| "#{key} (#{data_set.values[i]})" }
    img_src = Gchart.pie(:labels => labels, :data => data_set.values, :size => '750x400', :bg => '2f2f2f', :bar_colors => '336688')

    # Request length must be less than 2048, otherwise it will fail
    if img_src.length < 2048
      settings.graph_urls[label] = img_src
    else
      warn "!!! Request length for '#{label}' graph is too long. Skipping."
    end
  end
end