Railsで挫折した人のためのSinatra -6-
機能の追加と今後のブラッシュアップ
前回まででeasy-haikuが一応の完成を見せました。
しかし、今のままではかっこいいアプリケーションとはとても言えません。
ですので、UIに気を使ったりフロントエンドとバックエンドを完全に分離したり、これからのブラッシュアップはどんどんしたくなるでしょう。
バックエンドのほうでも新しいカラムを追加したりすると思います。
評価機能の追加
... といっていたら思い出したのですが、冒頭で俳句を評価する機能をつけると言って忘れていました。
せっかくですのでコチラを実装していきましょう!
まず評価したデータを保存しなければいけないのでtable設計を考えます。
assessmentsテーブルとして
こんなテーブルを設計します。
ここで、なぜ2つも別テーブルのカラムidを保持しているか気になると思うのですが、これは評価をしたuserのidと、どのhaikuを評価したかわかるようにするために評価したhaikuのidが必要だからです。これがあればuserからでもhaikuからでもassessmentからでもその他2つの結びついたデータを取得することができるようになります。
以下具体的なsqlです。(/db/init.sql)↓
DROP TABLE IF EXISTS haikus;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS assessments; -- 追加
create table haikus (
id INTEGER PRIMARY KEY AUTOINCREMENT,
main TEXT NOT NULL,
user_id INTEGER NOT NULL
);
create table users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
password TEXT NOT NULL
);
-- 追加
create table assessments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
haiku_id INTEGER NOT NULL,
score INTEGER NOT NULL
);
insert into users values (1, "takashi01", "0123456");
insert into haikus values (1, "古池や蛙飛こむ水のおと", 1);
insert into haikus values (2, "閑さや岩にしみ入蝉の声", 1);
insert into assessments values (1, 1, 1, 5); -- 追加
追加部分を追加して前章まででやってきたとおりsqlコマンドでsqlを実行してください。
sqlite3 〜 ではじまるコマンドです。
一番最後の行でassessmentsデータの挿入をしているのですが、一番最後にしないといけないのは、assessmentデータが、userのデータとhaikuのデータが存在することが前提だからです。順番に気をつけてください。
さて、DBのセットアップができたので、DBと接続するassessment modelを作りましょう!
/models以下にassessment.rbを作ってください。↓
require './models/base.rb'
class Assessment < ActiveRecord::Base
validates :score, presence: true
belongs_to :user
belongs_to :haiku
end
アソシエーションの設定をしたので、user、haikuにも対応するhas_manyを追加しましょう
/models/user.rb ↓
require './models/base.rb'
class User < ActiveRecord::Base
validates :name, presence: true
validates :password, presence: true
has_many :haikus, dependent: :destroy
has_many :assessments, dependent: :destroy # 追加
def authenticate(user_id)
return self.id == user_id ? true : false
end
end
/models/haiku.rb ↓
require './models/base.rb'
class Haiku < ActiveRecord::Base
validates :main, presence: true
belongs_to :user
has_many :assessments # 追加
end
これでActiveRecordを通しての、DBとの接続が出来ました。
続いて今設定したモデルを使って実際に動作を行うcontrollerを作りましょう。
/controllers以下にassessment.rbを作ってください。 ↓
require './controllers/base.rb'
require './models/assessment.rb'
class AssessmentController < Base
# 非ログイン時の投稿制限をしていたり、paramsとしてhaiku_idを取得したりしています。
post '/create' do
@haiku = Haiku.find(params[:haiku_id].to_i)
@assessments = Assessment.where(haiku_id: @haiku.id)
if session[:user_id]
@assessment = Assessment.new({score: params[:score].to_i, user_id: session[:user_id], haiku_id: params[:haiku_id].to_i})
if @assessment.save
redirect "/haiku/#{params[:haiku_id]}"
else
erb :show
end
else
# 未ログイン時にはエラーメッセージを返します。
@message = "You cannot post without logging in"
erb :show
end
end
post '/:id/delete' do
@assessment = Assessment.find(params[:id])
@assessment.delete
redirect "/haiku/#{@assessment[:haiku_id]}"
end
end
コントローラーを新しく追加したのでconfig.ruにも追記しましょう。
ここについてはご自身でやってみてください。
AssessmentControllerを/assessmentに割り当たるように書いてくだされば大丈夫です。
記述はファイル内の他の部分を参考にしてください。
config.ruの設定が終わったら、haikuの詳細ページで評価の投稿ができるようにしたいので、まず、/controllers/haiku.rbの/:id部分を以下のように修正します。
require './controllers/base.rb'
require './models/haiku.rb'
class HaikuController < Base
get '/' do
@haikulist = Haiku.all
erb :index
end
get '/:id' do
@haiku = Haiku.find(params[:id])
@assessments = Assessment.where(haiku_id: @haiku.id) # 追加部分
erb :show
end
post '/create' do
if session[:user_id]
@haiku = Haiku.new({main: params[:main], user_id: session[:user_id]})
if @haiku.save
redirect '/haiku'
else
if @haiku.errors.present?
erb :errors
end
end
else
@haikulist = Haiku.all
@message = "You cannot post without logging in"
erb :index
end
end
post '/:id/delete' do
@haiku = Haiku.find(params[:id])
@haiku.delete
redirect '/haiku'
end
end
追加部分は1箇所ですが、大事な記述なので忘れないように追加しましょう。
whereを使うことで@haikuのidを持つAssessmentを複数取得出来ます。
最後にviewsのshowページのerbファイルを修正します↓
<main>
<h1>Haiku details</h1>
<p>
<%= @haiku[:main] %>
<b>By</b><%= @haiku.user[:name] %>
</p>
<% if @haiku.user && session[:user_id] == @haiku.user[:id] %>
<form method="post" action=<%= "/haiku/#{@haiku[:id]}/delete" %>>
<input type="submit" value="Delete">
</form>
<% end %>
<h3>Assessments</h3>
<form method="post" action="/assessment/create">
<p>score:
<select name="score">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
pt
</p>
<input type="hidden" value=<%= @haiku.id %> name="haiku_id">
<p><input type="submit" value="Evaluate haiku"></p>
<% if @message %><p class="error-message"><%= @message %><p><% end %>
</form>
<% @assessments.each do |assessment| %>
<p>
<b>username</b> <%= assessment.user[:name] %></a>
<b>score</b> <%= assessment[:score] %>pt
</p>
<% if assessment.user && session[:user_id] == assessment.user[:id] %>
<form method="post" action=<%= "/assessment/#{assessment[:id]}/delete" %>>
<input type="submit" value="Delete">
</form>
<% end %>
<% end %>
</main>
<style>
.error-message {
color: red;
font-size: 14px;
}
</style>
- 操作しているユーザーからは見えないようにhaiku_idを渡している
- 評価は投稿したユーザーしか削除出来ない
の2点に注意しましょう!
以上で評価機能の追加は終了です。
dockerを再起動して動作確認してみましょう!
Javascriptの使用
今の状態では俳句詳細画面では評価をもらったら下に向かってどんどん表示されて長くなってしまいます。
通常時は評価部分を隠しておきましょう!
こんなときにJavascriptを使って画面の移動を伴わないDOM(Document Object Model)の操作を使うとクライアント側で楽しく画面を動かす事ができます。
はじめてJavascriptに触る方もjQueryなら触ったことがある方もそれ以外の方も今回は生のJavascriptでDOMの操作をしていきます。
具体的にどのように実装するかというと、hiddenというdisplay: none; を持ったcssのクラスを作ってそのクラスを、評価一覧を囲むdivタグと「show all assessments」という内容のpタグに付け替えて、表示させたり表示させ無かったりします。
それでは実装していきましょう!
特にJavascriptの書き方を覚えようとしなくて大丈夫です。
画面が動くのを楽しむのが目的です。
今回は/views/show.erbのみに書き込んでいきます。
...
<% if @message %><p class="error-message"><%= @message %><p><% end %>
</form>
<div id="assessments">
<% @assessments.each do |assessment| %>
<p>
<b>username</b> <%= assessment.user[:name] %></a>
<b>score</b> <%= assessment[:score] %>pt
</p>
<% if assessment.user && assessment.user.authenticate(session[:user_id]) %>
<form method="post" action=<%= "/assessment/#{assessment[:id]}/delete" %>>
<input type="submit" value="Delete">
</form>
<% end %>
<% end %>
<p id="hideText" class="clickText">hide all assessments</p>
</div>
<p id="openText" class="clickText">show all assessments</p>
</main>
<script>
</script>
<style>
.error-message {
color: red;
font-size: 14px;
}
.hidden {
display: none;
}
.clickText {
color: royalblue;
cursor: pointer;
text-decoration: underline;
}
</style>
上記のように/views/show.erbの後半部分、formの閉じタグ以降を修正します。
現在@assessmentsのeach文を囲んでいるdivタグにid=assessmensがあると思います。このidを利用してこのdivタグに.hiddenクラスをとったり付けたりします。
mainの閉じタグの上にあるid=openTextを持つpタグもそのdivタグと同様にします。
ちなみにその上のid=hideTextを持つpタグも利用します。
では<script></script>タグの中を以下のように記述しましょう
...
<script>
let assessments = document.getElementById("assessments");
let hideText = document.getElementById("hideText");
let openText = document.getElementById("openText");
console.log(assessments, hideText, openText)
</script>
...
記述が終わったらchromeの検証ツールのconsoleを開いて確認してみてください。
するとタグがたくさん表示されていると思います。console.log()という記述はカッコの中の変数をこのようにconsole上に表示することが出来ます。
console上に表示されているHTMLのタグのようなものがDOMです。
続いてクリックイベントの登録をしてみましょう。文字通りクリックすると何かが起こるように設定します。
...
<script>
let assessments = document.getElementById("assessments");
let hideText = document.getElementById("hideText");
let openText = document.getElementById("openText");
openText.addEventListener('click', () => {
console.log("click")
})
</script>
...
console上を見ながら、「show all assessments」という文をクリックしてみましょう。クリックするたびにconsole上にclickと表示されます。
そして、新たにisHidden変数を作って評価一覧を隠している状態と開いている状態をboolean型(true, false)で管理します。
このisHiddenとclickイベントを使うと状態の切り替えができるようになります。記述を修正してください
...
<script>
let assessments = document.getElementById("assessments");
let hideText = document.getElementById("hideText");
let openText = document.getElementById("openText");
let isHidden = true;
const attachClass = (isHidden) => {
if (isHidden) {
openText.classList.remove("hidden")
assessments.classList.add("hidden");
} else {
openText.classList.add("hidden")
assessments.classList.remove("hidden");
}
}
attachClass(isHidden)
openText.addEventListener('click', () => {
isHidden = !isHidden
attachClass(isHidden)
})
</script>
...
一気に記述が増えましたが、先程いったisHidden変数にtrueを入れたりfalseを入れたりして状態を切り替えています。
そして、attachClass関数でclassの付け替えを行っています。
実際に動かしてみると「show all assessments」をクリックすると、評価一覧が表示されると思います。
最後に閉じる動作も加えていきましょう。先程とってきていたhideTextのDOMをクリックイベントに登録すれば良さそうです。
...
<script>
let assessments = document.getElementById("assessments");
let hideText = document.getElementById("hideText");
let openText = document.getElementById("openText");
let isHidden = true;
const attachClass = (isHidden) => {
if (isHidden) {
openText.classList.remove("hidden")
assessments.classList.add("hidden");
} else {
openText.classList.add("hidden")
assessments.classList.remove("hidden");
}
}
attachClass(isHidden)
hideText.addEventListener('click', () => {
isHidden = !isHidden
attachClass(isHidden)
})
openText.addEventListener('click', () => {
isHidden = !isHidden
attachClass(isHidden)
})
</script>
...
記述したら、動かしてください。
「hide all assessments」の文字と「show all assessments」の文字を交互にクリックすることで開閉ができると思います。
簡単ですが、これでJavascriptの実装は終了です。
大切なのはDOMの取得とaddEventListenerを使ったイベントの追加です。
これができればひとまず、HTMLやerbファイルにJavascriptを実装出来ます。erbファイル内であればJavascriptの中でerb記法も使えたりするので色々試してみるのも良いでしょう。
最後に
今回作ったアプリケーションはuser.erbなどをあえて、不十分にものにしています。
本来ここでuserの名前やパスワードのアップデートをするのですが、その実装は応用課題とします。ご自身で実装してみてください。
ほかにも、パスワードがそのまま保存されていたりして大変危険なのでパスワードを隠して保存する等まだまだアプリケーションとして未完成なところがたくさん残っています。
それらを自分で調べつつ実装することで、調べる力であったり、お約束の書き方などに慣れていくとおもうので、是非そちらにも取り組んでいただけたらと思います。
バーっと書きなぐったのでおかしな文脈もあるとは思いますが、随時修正していきます。
何の気無しにかき始めたこのnoteですが、最後まで付き合ってくださった方々、途中まででも見てくださった方々、本当にありがとうございました。