New-Village

月間ブログ。だいたい1カ月に1回は更新しているようです。

RSSリーダー作成(データ表示)

今更ですが、ここまでの"アプリケーション作成"記事は、今日からスタートするRSSリーダー作成の為の布石として書いてきました。

アプリケーション作成(初期環境構築) - New-Village

アプリケーション作成(固定ページ作成) - New-Village

アプリケーション作成(認証導入①) - New-Village

 

今日から、これら記事で作成したページを母体に、RSSリーダーの機能実装に取り組んでいきます。アプリを作りながら記事を書くと結構シンドイので、端折っている部分があるかもしれませんが、ご愛嬌ということで。

それでは、作成に取りかかりたいと思います。

 

■ scaffoldの実行

ブランチを作成してからコントローラーを作成します。なお、今回はモデルも作成しますが最終的に複数のモデルを作成することになるのでscaffoldは使いませんでした。

なお、

~/workspace/xapp8(master)$ git checkout -b "board
~/workspace/xapp8(board)$ rails g scaffold main user_id:integer page_id:integer read_flg:integer --no-test-framework

コントローラーを作成したら、データベースを作成して、サーバーを起動してindex画面を確認してみます。

~/workspace/xapp8(board*)$ rake db:migrate
~/workspace/xapp8(board*)$ rails s

f:id:New-Village:20140818092406p:plain

Routing Errorが発生してしまいました。Routesに表示されている内容を確認してみると、"mains#index"を表示させるには"/mains"にgetでアクセスする必要がある…ということなので、改めて、"http://<your_host>:3000/mains"にアクセスしてみたら、正しく表示されました。

f:id:New-Village:20140818092724p:plain

 

■ 余談: データベースのカラムを変更

scaffoldで"page_id"というカラムを作成したが、よくよく考えると、"page_id"は別のページ管理に使い、ここのカラムは記事のidを振る必要があるので、変更することとした。

db/migrate/[timestamp]_create_mains.rb

class CreateMains < ActiveRecord::Migration
  def change
    create_table :mains do |t|
      t.integer :user_id
      t.integer :atcl_id
      t.integer :read_flg
      t.timestamps
    end
  end
end

そして、rakeしてから、カラムを修正して再度rake db:migrateをしても反映されないので、以下のようにコマンドを実行して修正した。

~/workspace/xapp8(board*)$rake db:migrate:reset

 

■ 余談: 小さな修正

ログイン後にルート画面が表示される必要は無い、というより、ルート画面は表示させない方が使いやすいので、コントローラーを少し変更して、ログイン後はルート画面に行けないようにしました(また、ヘッダーとフッダーのレイアウトも変更していますが、ここでは記述しません)。

f:id:New-Village:20140818093937p:plain

static_pagesコントローラーのhomeメッソドにログイン状態を確認して、ログイン中ならmainsにリダイレクトをするように書きます。

app/controllers/static_pages_controller.rb

class StaticPagesController < ApplicationController
  def home
    #ログイン状態でhomeにアクセスした場合
    if user_signed_in?
      redirect_to mains_path
    end
  end
...
end

 

■ deviseログイン後のリダイレクト設定

メインページの作成が完了したら、application_controllerにログイン・ログアウト後のリダイレクト先を指定します。 

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  ...
  #ログイン後のリダイレクト先
  def after_sign_in_path_for(resource)
    main_path
  end

  #ログアウト後のリダイレクト先
  def after_sign_out_path_for(resource)
    root_path
  end

end

また、 MainsController配下へのアクセスはログイン後にのみアクセスされるもののため、filterをセットします。これで、Board配下のページに直接アクセスがあった場合は、ログインページにリダイレクトされるようになります。

class MainsController < ApplicationController
  #ログインしているかどうか確認
  before_filter :authenticate_user!
  before_action :set_main, only: [:show, :edit, :update, :destroy]
  ...
end

 

■ 見た目を編集①

アプリケーションの基本画面はindex.html.erbを使おうと考えているので、見た目を簡単に修正してみました。

f:id:New-Village:20140818133347p:plain

app/views/mains/index.html.erb

<div class="row">
  <aside class="col-md-3">.col-md-3</aside>
    <div class="col-md-9">
      <h1>ヘルプ</h1>
      <p>
        このページの作成過程はブログ
        <a href="http://new-village.hatenablog.com/">新しい村</a>
        で記録しています。また、このソースコードは、
        <a href="https://github.com/Kzki/xapp9">こちら</a>
        から確認することができます。
      </p>
    </div>
</div>

app/assets/stylesheets/mains.css.scss

/* mixins, variables, etc. */
$grayMediumLight: #eaeaea;
/* universal */
div.row {
  height: calc(100% - 32px);
  width: 100%;
}
aside {
  background-color: $grayMediumLight;
  height: 100%;
}

 app/assets/stylesheets/custom.css.scss

...
/* universal */
html {
  overflow-y: scroll;
  height: 100%;
}
body {
  padding-top: 50px;
  height: 100%;
}
...

ついでに app/assets/images/ に、favicon.icoをアップロードしてファビコンを表示できるようにファイルを修正していました(ファビコンの配置場所はバージョンや設定によって変わるようです。ネット上ではpublicに置くという解説も出てますが、私の環境では、assetsの下でした)。

app/views/layouts/application.html

<!DOCTYPE html>
<html>
<head>
<title><%= full_title(yield(:title)) %></title>
<%= favicon_link_tag %>
<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %>
<%= javascript_include_tag "application", "data-turbolinks-track" => true %>

...

 

■ モデル開発

現時点でのデータモデルは以下のような構造を考えています。

mainsテーブルをログインしているユーザかつ、read_flgが0(未読)のatcl_idを取得して、articlesテーブルとjoinすることで、記事の一覧が取得できると考えています。

備忘録的に書いておきますが、articles.page_idを使ってページ情報を取得しようと思っていますが、ログインユーザからmainsを抽出してarticlesを取得、その後にpage_idでdistinctしてからページ情報を取得すると遅くなりそうなので…ユーザからページ情報を一気に取得できる流れを作るべきか悩んでいます。

f:id:New-Village:20140818115316p:plain

まずは、開発用データを投入します。fekerとか使えば楽かもしれないけど、とりあえず、開発時のトライアンドエラー用に数件作成した。

~/workspace/xapp8(board*)$ rails dbconsole
sqlite> insert into mains values(1,1,1,0,date(),date());
sqlite> insert into mains values(2,1,2,0,date(),date());
sqlite> insert into mains values(3,1,3,1,date(),date());
sqlite> insert into mains values(4,2,1,0,date(),date()); 
sqlite> select * from mains;
  1|1|1|0|2014-08-18|2014-08-18
  2|1|2|0|2014-08-18|2014-08-18
  3|1|3|1|2014-08-18|2014-08-18
  4|2|1|0|2014-08-18|2014-08-18
sqlite> .quit

次に、mainsとusersの関係性をモデルに定義します。mainモデルはデフォルトソートも設定します。

app/models/main.rb

class Main < ActiveRecord::Base
  belongs_to :user
  default_scope -> { order('created_at DESC') }

end

app/models/user.rb

class User < ActiveRecord::Base
  ...
  has_many :mains, dependent: :destroy
end

とりあえず、一覧表示できるようにコントローラーとビューを設定して、画面を見てみます。

app/views/main/index.html.erb

<div class="row">
  <aside class="col-md-3">.col-md-3</aside>
  <div class="col-md-9">
    <h1>記事一覧</h1>
    <% @main.each do |i| %>
      <li>
        <%= i.id %>
        <%= i.user.email.split("@")[0] %>
        <%= i.atcl_id %>
        <%= case i.read_flg when 0 then "未読" when 1 then "既読" end %>
      </li>
    <% end %>
  </div>
</div>

app/controllers/mains_controller.rb

class MainsController < ApplicationController
...
  def index
    @mains = Main.where(:user_id => current_user.id)
  end 

...

f:id:New-Village:20140818170931p:plain

 

■ 余談: レコードの検索方法

コントローラーの記載方法によって、ビューの書き方が変わる。自分の理解が至って居ないのが問題なんだろうが、いずれにしても、数時間も"undefined method `each' for #<Main:0x007fc8a2a5b410>"というNoMethodErrorと格闘することになったので、記録を残しておく。

まずは、whereを使ってレコード検索した場合、もしくはallを使って全件取得した場合は複数行帰ってくることが前提になるので、配列で帰ってくる。このため、"each"を使ってレコード毎に取り出してあげないとエラーになってしまう。

コントローラー

def index
  @mains = Main.where(:user_id => current_user.id)
end

 

ビュー

<% @main.each do |i| %>
  <li><%= i.id %></li>
<% end %>

一方で、find_byは一行しか帰ってこない(Limit 1)ので、"each"を使うと逆にエラーになる。

コントローラー

def index
  @mains = Main.find_by(:user_id => current_user.id)
end

ビュー

<%= @user.email %>

 

■ モデル開発(その2)

usersとmainsモデルの開発が完了したので、次に記事(articles)モデルを作成して、indexページに表示できるようにしていきます。

まずは、モデルを作成してサンプルデータを投入していきます。

~/workspace/xapp8(board*)$ rails g model Article title:string url:string desc:string subject:string creator:string date:datetime page_id:in
teger
~/workspace/xapp8(board*)$ rake db:migrate
~/workspace/xapp8(board*)$ rails dbconsole 
sqlite> insert into articles values(1,"アプリケーション作成(固定ページ作成)","app.com","I am","","",date(),1,date(),date());
sqlite> insert into articles values(2,"アプリケーション作成(初期環境構築)","app.com","I am","","",date(),1,date(),date());
sqliteinsert into articles values(3,"bootstrap3-sassで一悶着","app.com","I am","","",date(),1,date(),date());
sqlite> .quit

投入が終わったら、モデルに関係性を定義していきます。Main.atcl_id と Article.id でLetf Joinするので、フォーリンキーの設定を忘れずにします。

app/models/main.rb

class Main < ActiveRecord::Base
  belongs_to :user
  belongs_to :article, :foreign_key => "atcl_id"
  default_scope -> { order('created_at DESC') }
end

app/models/article.rb

class Article < ActiveRecord::Base
  has_many :mains, dependent: :destroy
end

最後にHTMLを修正して一覧表示は完了です。

app/views/main/index.html.erb

...
    <% @main.each do |i| %>
      <li>
        <%= i.id %>
        <%= i.user.email.split("@")[0] %>
        <%= i.article.title %>
        <%= case i.read_flg when 0 then "未読" when 1 then "既読" end %>
      </li>
    <% end %>
...

f:id:New-Village:20140818184326p:plain

 

■ サンプルデータの生成

いろいろとテストをするために、サンプルデータを自動で大量生成できるようにします。

gemfileにfakerを加えてインストールします。開発テストでしか使わないので、以下の位置に加えます。

gemfile

...
group :development, :test do
  gem 'sqlite3', '~>1.3'
  gem 'faker', '~>1.4'
end
...

gemfileを編集し終わったら、インストールします。

~/workspace/xapp8(board*)$ bundle update

fakeのインストールが完了したら、lib/tasks/ 配下にsample_data.rakeというファイルを作成して、ユーザデータを自動生成できるように編集します。

lib/tasks/sample_data.rake

namespace :db do
  desc "ユーザーの作成"
  task populate: :environment do
    User.create!(email: "sample@xapp8.com",
      password: "password",
      password_confirmation: "password")

    2.times do |n|
      email = Faker::Internet.free_email
      password = "password"
      User.create!(email: email,
        password: password,
        password_confirmation: password)
    end

    300.times do |n|
      title = Faker::Lorem.sentence
      url = Faker::Internet.url
      desc = Faker::Lorem.paragraph(5)
      date = DateTime.new(rand(2013..2015), rand(1..12) , rand(1..30), rand(0..23), rand(0..59), rand(0..59))
      page_id = rand(1..10)
      Article.create!(title: title,
        url: url,
        desc: desc,
        subject: "",
        creator: "",
        date: date,
        page_id: page_id)
    end

    300.times do |n|
      user_id = rand(3)
      atcl_id = "#{n+1}".to_i
      read_flg = rand(2)
      Main.create!(user_id: user_id,
        atcl_id: atcl_id,
        read_flg: read_flg)
    end

  end
end

rakeファイルのセットアップが終わったら、データベースをリセットしてデータを投入します。

~/workspace/xapp8(board*)$ rake db:reset
~/workspace/xapp8(board*)$ rake db:populate
~/workspace/xapp8(board*)$ rake test:prepare

 

■ レンダー化

右側の記事一覧をレンダー化して、かつスクロールするように修正した。

app/views/main/index.html.erb

<div class="row">
  <aside class="col-md-3">
    <ul class="nav nav-pills nav-stacked">
      <li class="active"><a href="#"><span class="badge pull-right"><%= @main.count %></span>未読</a></li>
    </ul>
  </aside>
  
  <div class="col-md-9">
    <div class="panel-group" id="accordion">
      <div class="panel panel-default">
        <%= render @main %>
      </div>
    </div>
  </div>
</div>

app/views/main/_main.html.erb

<div class="panel-heading" data-toggle="collapse" data-parent="#accordion" href='<%= "#"+main.id.to_s %>'>
  <h4 class="panel-title">
    <%= link_to main.article.title, main_path(main.id), :method => "PATCH" %>
  </h4>
</div>
<div id='<%=main.id.to_s %>' class="panel-collapse collapse">
  <div class="panel-body">
    <%= main.id %>,<%= case main.read_flg when 0 then "未読" when 1 then "既読" end %>,<%= main.article.desc %>
  </div>
</div>

app/assets/stylesheets/mains.css.scss

/* bootstrap gridsystem */
div.row {
  height: calc(100% - 26px);
  width: 100%;
  margin: 0px;
}

div.col-md-9 {
  height: 100%;
  padding-left: 5px;
  padding-right: 20px;
  overflow: auto;
}

/* bootstrap Collapse */
.panel-body {
  border-top: 0px !important;
}

.panel-group .panel-heading {
  border-bottom: 1px solid #dddddd;
}

bootstrapのjavascriptを有効化する為に、"require bootstrap"を"application.js"に追加します。
app/assets/javascripts/application.js 

//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require bootstrap
//= require_tree .

見た目は別として、一覧表示する画面ができましたので、とりあえずここまでで一段落とします。

f:id:New-Village:20140819124355p:plain

 

■ 最後に

githubとherokuの環境を更新して終わりにします。疲れた〜。 

~/workspace/xapp8(board*)$ git add .
~/workspace/xapp8(board)$ git commit -m"一覧画面作成" 
~/workspace/xapp8(board)$ git checkout master
~/workspace/xapp8(master)$ git merge board
~/workspace/xapp8(master)$ git push
~/workspace/xapp8(master)$ git push heroku
~/workspace/xapp8(master)$ heroku rake db:migrate