Rails4でActiveAdminとdeviseとcancancanをつかって管理者ページを作る

環境

  • Rails 4.1.2
  • ruby 2.1.1
  • devise 3.3.0
  • active_admin 1.0.0 pre
  • cancancan 1.9.2

Gemfile

gem 'rails', '4.1.2'
gem 'cancancan'
gem 'devise'
gem 'activeadmin', github: 'activeadmin'

bundleする

$ bundle install

まずはdeviseでモデルを作る. roleでadminかどうかを判定するのでUserコントローラに追加

$ rails generate devise:install
$ rails generate devise User
$ rails generate migration add_role_to_users role:integer
$ rake db:migrate

Deviseのモデルを使うのでActiveAdminでUserモデルをつくらないようにするために--skip-users

$ rails generate active_admin:install --skip-users
 create  config/initializers/active_admin.rb
      create  app/admin
      create  app/admin/dashboard.rb
       route  ActiveAdmin.routes(self)
    generate  active_admin:assets
      create  app/assets/javascripts/active_admin.js.coffee
      create  app/assets/stylesheets/active_admin.css.scss
      create  db/migrate/20140829091410_create_active_admin_comments.rb
$ rake db:migrate

これするとconfig/intializers/active_admin.rbのなかを幾つか変えないといけない

config.current_user_method = :current_admin_user
↓
config.current_user_method = :current_user

config.logout_link_path = :destroy_admin_user_session_path
↓
config.logout_link_path = :destroy_user_session_path

またapp/application_controller.rbに以下を追加

def authenticate_admin_user!
  authenticate_user!
  unless current_user.admin?
    flash[:alert] = "This area is restricted to administrators only."
    redirect_to root_path
  end
end

userモデルはadmin?は持ってないので以下をmodel/user.rbに追加(Rails 4からenumが使える)

enum role: %(admin normal)

cancancanいらなかった!!!

roleが3つ以上あるといる気がするけどめんどくさそう

参考

Railsで新規にWebサービスを立ち上げる際にやったことまとめ [Rails] Devise and Active Admin Single User Model

集合知プログラミング 2章をrubyで書いた

書きました。

APIを使うところは省略しています

critics = {
  'Lisa Rose' => { 'Lady in the Water' => 2.5, 'Snakes on a Plane' => 3.5, 'Just My Luck' => 3.0, 'Superman Returns' => 3.5, 'You, Me and Dupree' => 2.5, 'The Night Listener' => 3.0 },
  'Gene Seymour' => { 'Lady in the Water' => 3.0, 'Snakes on a Plane' => 3.5, 'Just My Luck' => 1.5, 'Superman Returns' => 5.0, 'The Night Listener' => 3.0, 'You, Me and Dupree' => 3.5 },
  'Michael Phillips' => { 'Lady in the Water' => 2.5, 'Snakes on a Plane' => 3.0, 'Superman Returns' => 3.5, 'The Night Listener' => 4.0 },
  'Claudia Puig' => { 'Snakes on a Plane' => 3.5, 'Just My Luck' => 3.0, 'The Night Listener' => 4.5, 'Superman Returns' => 4.0, 'You, Me and Dupree' => 2.5 },
  'Mick LaSalle' => { 'Lady in the Water' => 3.0, 'Snakes on a Plane' => 4.0, 'Just My Luck' => 2.0, 'Superman Returns' => 3.0, 'The Night Listener' => 3.0, 'You, Me and Dupree' => 2.0 },
  'Jack Matthews' => { 'Lady in the Water' => 3.0, 'Snakes on a Plane' => 4.0, 'The Night Listener' => 3.0, 'Superman Returns' => 5.0, 'You, Me and Dupree' => 3.5 },
  'Toby' => { 'Snakes on a Plane' => 4.5, 'You, Me and Dupree' => 1.0, 'Superman Returns' => 4.0 } }

def sim_distance(prefs, person1, person2)
  si = prefs[person1].select { |k, _v| prefs[person2].key?(k) }.keys
  return 0 if si.size.zero?

  sum_of_square = si.inject(0) do |a, e|
    a + (prefs[person1][e] - prefs[person2][e])**2
  end

  1 / (1 + sum_of_square)
end

def sim_piason(prefs, person1, person2)
  si = prefs[person1].keys.select { |k| prefs[person2].key?(k) }
  n = si.size
  return 0 if n.zero?

  sum1 = si.inject(0) { |a, e| a + prefs[person1][e] }
  sum2 = si.inject(0) { |a, e| a + prefs[person2][e] }
  p_sum = si.inject(0) { |a, e| a + (prefs[person2][e] * prefs[person1][e]) }

  sum1_sq = si.inject(0) { |a, e| a + prefs[person1][e]**2 }
  sum2_sq = si.inject(0) { |a, e| a + prefs[person2][e]**2 }

  s_xy = p_sum - (sum1 * sum2 / n)
  s_xx = sum1_sq - (sum1**2 / n)
  s_yy = sum2_sq - (sum2**2 / n)

  return 0 if Math.sqrt(s_xx * s_yy).zero?

  s_xy / Math.sqrt(s_xx * s_yy)
end

def top_match(pref, person, n = 5, similarity = method(:sim_piason))
  personp = ->(x) { x == person }
  cal_sim = ->(p) { [p, similarity.call(pref, p, person)] }
  descend = ->((_, p1), (_, p2)) { p2 <=> p1 }

  pref.keys.reject(&personp).map(&cal_sim).sort(&descend)[0...n]
end

def get_recommendation(pref, person, similarity = method(:sim_piason))
  personp = ->(x) { x == person }
  cal_sim = ->(p) { [p, similarity.call(pref, p, person)] }
  gt_zero = ->(x) { x[1] > 0 }
  person_has_item = ->(x) { pref[person].key?(x) }

  totals = {}
  sim_sum = {}

  pref.keys.reject(&personp).map(&cal_sim).select(&gt_zero).each do |other, sim|
    pref[other].keys.reject(&person_has_item).each do |item|
      totals[item] = (totals[item] || 0) +  pref[other][item] * sim
      sim_sum[item] = (sim_sum[item] || 0) + sim
    end
  end

  totals.map do |(k, v)|
    [(v / sim_sum[k]), k]
  end.sort.reverse
end

def transform_prefs(prefs)
  prefs.each_with_object({}) do |(person, items), a|
    items.each do |movie, v|
      a[movie] ||= {}
      a[movie][person] = v
    end
  end
end

def calculate_similar_item(prefs, n = 10)
  item_pref = transform_prefs(prefs)

  item_pref.keys.each_with_object({}) do |item, a|
    a[item] = top_match(item_pref, item, n, method(:sim_distance))
  end
end

def get_recommended_items(prefs, item_match, user)
  user_rating = prefs[user]
  scores = {}
  total_sim = {}

  user_rating.each do |item, rating|
    item_match[item].reject { |item2, _| user_rating.keys.include?(item2) }.each do |item2, sim|
      scores[item2] = (scores[item2] || 0) + sim * rating
      total_sim[item2] = (total_sim[item2] || 0) + sim
    end
  end

  scores.map do |item, score|
    [(score / total_sim[item]), item]
  end.sort.reverse
end

if __FILE__ == $PROGRAM_NAME
  # p transform_prefs(critics)
  # p sim_piason(critics, 'Lisa Rose', 'Gene Seymour')
  # p sim_distance(critics, 'Lisa Rose', 'Gene Seymour')
  # p top_match(critics, 'Toby', 3)
  # p get_recommendation(critics, 'Toby')

  item_sim = calculate_similar_item(critics)
  p get_recommended_items(critics, item_sim, 'Toby')
end

今日解いた問題3

今日というか今日と昨日

単一始点最短経路問題(ベルマンフォード)

ある頂点sからのすべての頂点の最短経路 d[j] > d[i] + costこの条件を1度使っただけでは明らかに最短にならないので update変更がなくなるまでループを繰り返す

INF = 100_000

G = [[INF, 2, 5, INF, INF, INF, INF],
     [2, INF, 4, 6, 10, INF, INF],
     [5, 4, INF, 2, INF, INF, INF],
     [INF, 6, 2, INF, INF, 1, INF],
     [INF, 10, INF, INF, INF, 3, 5],
     [INF, INF, INF, 1, 3, INF, 9],
     [INF, INF, INF, INF, 5, 9, INF]]

def shotest_path(s)
  d = Array.new(G.size, INF)

  d[s] = 0

  loop do
    update = false

    G.each_with_index do |row, i|
      row.each_with_index do |cost, j|
        next if cost == INF || d[i] == INF

        if d[j] > d[i] + cost
          d[j] = d[i] + cost
          update = true
        end
      end
    end

    break unless update
  end

  d
end

p shotest_path(0) # => [0, 2, 5, 7, 11, 8, 16]

単一始点最短経路問題(ダイクストラ)

ベルマンフォードと目的は一緒
まだ訪れていない頂点の中から最短の頂点を見つけてそこから他の頂点への経路のコストを得る

INF = 100_000

G = [[INF, 2, 5, INF, INF, INF, INF],
     [2, INF, 4, 6, 10, INF, INF],
     [5, 4, INF, 2, INF, INF, INF],
     [INF, 6, 2, INF, INF, 1, INF],
     [INF, 10, INF, INF, INF, 3, 5],
     [INF, INF, INF, 1, 3, INF, 9],
     [INF, INF, INF, INF, 5, 9, INF]]

def dijkstra(s)
  size = G.size
  d = Array.new(size, INF)
  used =  Array.new(size, false)
  d[s] = 0

  loop do
    min_node = -1

    size.times do |i|
      min_node = i if !used[i] && (min_node == -1 || d[min_node] > d[i])
    end

    break if min_node == -1
    used[min_node] = true

    size.times do |i|
      d[i] = [d[min_node] + G[min_node][i], d[i]].min
    end
  end

  d
end

p dijkstra(0) # => [0, 2, 5, 7, 11, 8, 16]

全点対最短経路( ワーシャルフロイド)

すべての頂点からそれ以外のすべての頂点の最短経路
iからjまでのパスの中で頂点k通るか通らないかのDP
通るときはg[i][k] + g[k][j]で通らない時はg[i][j]
ホントはd[k][i][j]とかなるんだけどループ内で省略してる

INF = 100_000

def warshall_floyd

  g = [[0, 2, 5, INF, INF, INF, INF],
       [2, 0, 4, 6, 10, INF, INF],
       [5, 4, 0, 2, INF, INF, INF],
       [INF, 6, 2, 0, INF, 1, INF],
       [INF, 10, INF, 0, INF, 3, 5],
       [INF, INF, INF, 1, 3, 0, 9],
       [INF, INF, INF, INF, 5, 9, 0]]

  size = g.size

  size.times do |k|
    size.times do |i|
      size.times do |j|
        g[i][j] = [g[i][j], g[i][k] + g[k][j]].min
      end
    end
  end

  g
end

p warshall_floyd # [[0, 2, 5, 7, 11, 8, 16], [2, 0, 4, 6, 10, 7, 15], [5, 4, 0, 2, 6, 3, 11], [7, 6, 2, 0, 4, 1, 9], [7, 6, 2, 0, 4, 1, 5], [8, 7, 3, 1, 3, 0, 8], [12, 11, 7, 5, 5, 6, 0]]

アリ本のP102
2番めの最短経路をだす問題
priority_queueがなかったのでかなり汚い感じになった
基本方針はすべての頂点に対して2番めの最短経路も持っとくこと

N = 4
M = 4
INF = 100_000

G = [[INF, 100, INF, INF],
     [100, INF, 250, 200],
     [INF, 250, INF, 100],
     [INF, 200, 100, INF]]

def solve
  d1 = Array.new(N, INF)
  used1 = Array.new(N, false)
  d2 = Array.new(N, INF)
  used2 = Array.new(N, false)

  d1[0] = 0
  d2[0] = 0

  loop do
    v = [-1, 1]

    N.times do |i|
      v = [i, 1] if !used1[i] && (v[0] == -1 || d1[v[0]] > d1[i])
      v = [i, 2] if !used2[i] && (v[0] == -1 || d2[v[0]] > d2[i])
    end

    break if v[0] == -1
    eval("used#{v[1]}[#{v[0]}]=true")

    N.times do |i|
      d = eval("d#{v[1]}[#{v[0]}]") + G[v[0]][i]

      if d < d1[i]
        d, d1[i] = d[i], d
      end

      if d > d1[i] && d < d2[i]
        d2[i] = d
      end
    end
  end

  d2[N-1]
end

p solve # => 450

今日解いた問題2

彩色問題

アリ本の93ページ
隣接したりノードが同じ色にならないように色をぬる。
今回は2色で塗ることができるかという問題
特に2色でぬれるグラフを2部グラフという

コードで言うと0が塗ってないノードなので下の2つを繰り返す感じ

  • 隣接したノードjが0ならノードjに対して色(-c)を塗る
  • 隣接したノードjと現在のノードがiが同じ色c(1 or 0)ならfalseを返す
G1 = [[0, 1, 1],
      [1, 0, 1],
      [1, 1, 0]].freeze

G2 = [[0, 1, 0, 1],
      [1, 0, 1, 0],
      [0, 1, 0, 1],
      [1, 0, 1, 0]].freeze

s = G2.size
$color = Array.new(s, 0)         # 0は塗ってない 1と-1が塗った

def dfs(i, c)
  $color[i] = c

  G2[i].each_with_index do |e, j|
    next unless e == 1
    return false if $color[j] == c
    return false if $color[j].zero? && !dfs(j, -c)
  end

  true
end

s.times do |i|
  next unless $color[i].zero?
  unless dfs(i, 1)
    puts 'NO'
    break
  end
  puts 'YES'
end

今日解いた問題

分割数はいまいちわかっていない

最長増加部分列問題

自分(i)より小さい部分問題を解いていけばできる

N = 5
A = [4, 2, 3, 1, 5].freeze

$dp = Array.new(N, 1)

def solve
  N.times do |i|
    i.times do |j|
      $dp[i] = [$dp[j] + 1, $dp[i]].max if A[i] > A[j]
    end
  end

  $dp.max
end

p solve # => 3

分割数

NをM個以下に分割する総数

N=4,M=3の場合 dp(3,4)=dp(3,1)+dp(2,4)になる.
dp(2,4)は総数なので2個以下で分割するときの総数($dp[i-1][j])
dp(3,1)は3分割したもののそれぞれから1引いてそれを分割する総数($dp[i][j-1] この場合 j < iなので0)
3分割したものから1引くとは4=1+1+2となるのでそれぞれから1引いて0+0+1=1

N = 4
M = 3

$dp = Array.new(M + 1).map { Array.new(N + 1, 0) }

def solve
  $dp[0][0] = 1

  (1..M).each do |i|
    (0..N).each do |j|
      a = j - i >= 0 ? $dp[i][j-i] : 0
      $dp[i][j] =  $dp[i-1][j] + a
    end
  end

  $dp[M][N]
end

p solve # => 4

ナップザック問題

久しぶりにナップザック問題書いてみたら思いの外時間がかかって辛かった

N = 4
W = 5
ITEMS = [[2, 3], [1, 2], [3, 4], [2, 2]].freeze # [重さ, 価値]
INF = 10000000

# 全探索
def dfs(i, w)
  return 0 if i == N
  b = dfs(i + 1, w)
  a = w - ITEMS[i][0] >= 0 ? dfs(i + 1, w - ITEMS[i][0]) + ITEMS[i][1] : 0
  [a, b].max
end
p dfs(0, W) # => 7 


# memo化
$memo = Array.new(N).map { Array.new(N, INF) }
def dfs_memo(i, w)
  return 0 if i == N

  b = dfs_memo(i + 1, w)
  a = w - ITEMS[i][0] >= 0 ? dfs_memo(i + 1, w - ITEMS[i][0]) + ITEMS[i][1] : 0
  $memo[i][w] = [a, b].max
end
p dfs_memo(0, W) # => 7 



# dp
$dp = Array.new(N + 1).map { Array.new(W + 1, 0) }
def solver
  (1..N).each do |i|
    (1..W).each do |j|
      a = $dp[i-1][j]
      b = j >= ITEMS[i-1][0] ? $dp[i-1][j-ITEMS[i-1][0]] + ITEMS[i - 1][1] : 0
      $dp[i][j] = [a, b].max
    end
  end
end

solver
p $dp[N][W] # => 7 

rspec3で標準入力と標準出力のテスト

rspecで標準入力からユーザの入力を受け取って標準出力に出力する方法の覚書 rspec2とrspec3がまざっててややこしかった

プロダクションコード

solver.rb

getでユーザの入力を受け取ってprintで標準出力に答え(文字列)を出力するのクラス

class Solver
  attr_reader :n

  def get
    @n = gets
  end

  def print
    puts 'answer is foo'
  end
end

テストコード

solver_spec.rb

標準入力ではallow(ARGF).to receive(:gets) { 1000 }とするといいらしい. 1000の部分を入力したい値に変更すればいい.

標準出力はoutput("answer is foo\n").to_stdoutとするといいらしい. "answer is foo\n"の部分を出力したい値に変更すればいい.

require 'solver'

describe Solver do
  let(:solver) { described_class.new }

  describe 'solver#getは標準入力を受け取って@nにセットする' do
    before do
      allow(ARGF).to receive(:gets) { 1000 }
      solver.get
    end

    it { expect(solver.n).to eq 1000 }
  end

  describe 'solver#printは標準出力に答えを出力する' do
    it { expect { solver.print }.to output("answer is foo\n").to_stdout }
  end
end

参考