中ボスから話もあったので、Ruby on Railsの初アプリとして以下のような感じで飲み会管理システムを作ることにした。
飲み会管理システム = MeetingManager = mm を作ってみよう。
% rails mm
作られたconfig/database.ymlのままで問題はないので、データベース作成
% mysql -u root mysql> create database mm_development; mysql> create database mm_test; mysql> create database mm_production; mysql> quit
モデルを作成
% ruby script/generate model user % ruby script/generate model event % ruby script/generate model membership
db/migrateに、テーブルを作ったり消したりするファイルも出来たので、self.upの中を更新。 ユーザに見せる会員番号のためにaccountnumberを入れた。
class CreateUsers < ActiveRecord::Migration def self.up options = { :options => "DEFAULT CHARSET=utf8" } create_table(:users, options) do |t| t.column :nickname, :string t.column :accountnumber, :string t.column :mailaddress, :string t.column :root, :boolean, :default => 1 end end
宴会の案内はメールしたいので、maibodyでtextにした。
class CreateEvents < ActiveRecord::Migration def self.up options = { :options => "DEFAULT CHARSET=utf8" } create_table(:events, options) do |t| t.column :title, :string t.column :mailbody, :text end end
宴会に招待されるとmembershipが作られ、参加するとjoinedがtrueになる。 user_id, event_idは外部キーにしたいが、よく分からないので、integerにする。
class CreateMemberships < ActiveRecord::Migration def self.up create_table(:memberships, options) do |t| t.column :user_id, :integer t.column :event_id, :integer t.column :joined, :boolean, :default => 0 end
実行。
% rake db:migrate
確かめてみる。
% mysql -u root mm_development mysql> describe events; +----------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +----------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | title | varchar(255) | YES | | NULL | | | mailbody | text | YES | | NULL | | +----------+--------------+------+-----+---------+----------------+
管理者を一人、一般ユーザを一人足しておこう。
% ruby script/generate migration AddDefaultUsers class AddDefaultUsers < ActiveRecord::Migration def self.up User.create(:nickname => 'root', :accountnumber => '0000', :mailaddress => 'root@example.com', :root => 1) User.create(:nickname => 'test', :accountnumber => '9999', :mailaddress => 'test@example.com', :root => 0) end % rake db:migrate
確認してみる。
% mysql -u root mm_development mysql> select * from users; +----+----------+---------------+------------------+------+ | id | nickname | accountnumber | mailaddress | root | +----+----------+---------------+------------------+------+ | 1 | root | 0000 | root@example.com | 1 | | 2 | test | 9999 | test@example.com | 0 | +----+----------+---------------+------------------+------+
scaffoldを作る。
% ruby script/generate scaffold User User % ruby script/generate scaffold Event Event % ruby script/generate scaffold Membership Membership
サーバを起動して
% ruby script/server
http://0.0.0.0:3000/userにアクセスすると
Listing users Nickname Accountnumber Mailaddress Root root 0000 root@example.com true Show Edit Destroy test 9999 test@example.com false Show Edit Destroy New user
が出た。すげー。
メインの処理用にMMコントローラを作る。
ruby script/generate controller MM login logout edit update
ザルな認証のために、フォームからnickname, accountnumberを入れさせる。
<h1>MM#login</h1> <p>Find me in app/views/mm/login.rhtml</p> <%= form_tag %> ニックネーム <%= text_field("user", "nickname") %> 会員番号 <%= text_field("user", "accountnumber") %> <input type="submit" value="ログイン" /> <%= end_form_tag %>
controllerでは、try_to_loginが通ったら、"edit"に移る。 この辺は、RailsによるアジャイルWebアプリケーション開発のまま。
class MMController < ApplicationController def login if request.get? session[:user_id] = nil @user = User.new else @user = User.new(params[:user]) logged_in_user = @user.try_to_login if logged_in_user session[:user_id] = logged_in_user.id redirect_to(:action => "edit", :id => logged_in_user.id) else flash[:notice] = "invalid" end end end
models/user.rbでは、nickname, accountnumberが一致したら、try_to_loginで該当userを返す。
class User < ActiveRecord::Base def self.login(nickname, accontnumber) find(:first, :conditions => ["nickname = ? and accountnumber = ?", nickname, accontnumber]) end def try_to_login User.login(self.nickname, self.accountnumber) end end
http://0.0.0.0:3000/mm/login にアクセスしたら
LoadError in MmController#login app/controllers/mm_controller.rb to define MmController
と言われた。
mm_controller.rbでは、MmControllerではなく、MMControllerが定義されている。 2文字のコントローラを作ったのが失敗だったか。MmControllerに修正しよう。
ログインに成功すると
MM#edit Find me in app/views/mm/edit.rhtml
が出るようになった。
管理者として認証にパスしたら、という条件は後で考えるとして、飲み会管理画面からユーザを誘えるようにしよう。 とりあえず、editのときだけ誘えればいいや。
models/event.rbで、membershipと招待フラグ、参加フラグをhashで返せるようにしておく。
class Event < ActiveRecord::Base def memberships mhash = Hash::new m = Membership.find(:all, :conditions => ["event_id = ?", self.id]) m.each do |membership| mhash[membership.user_id] = membership end return mhash end def proposes phash = Hash::new memberships.each do |id, membership| phash[membership.user_id] = true end return phash end def joins jhash = Hash::new memberships.each do |id, membership| jhash[membership.user_id] = membership.joined end return jhash end end
event_contoller.rbで変数に入れて
def edit @event = Event.find(params[:id]) @users = User.find(:all) @memberships = @event.memberships @proposes = @proposes.joins @joins = @event.joins end
event/edit.rhtmlで表示する。 form_tagが、end_form_tagでなくendで閉じられているようだけど、いいのかな。
<h1>Editing event</h1> <% form_tag :action => 'update', :id => @event do %> <%= render :partial => 'form' %> <table> <tr><td>ニックネーム</td><td>招待</td><td>参加</td></tr> <% @users.each do |user| %> <tr> <td><%= user.nickname %></td> <td><%= check_box("proposed", user.id, {:checked => @proposes[user.id]}, "yes", "no") %></td> <td><%= check_box("joined", user.id, {:checked => @joins[user.id]}, "yes", "no") %></td> </tr> <% end %> <%= submit_tag '更新' %> <% end %> <%= link_to 'Show', :action => 'show', :id => @event %> | <%= link_to 'Back', :action => 'list' %>
event_contoller.rbではmembershipを更新。
def update @event = Event.find(params[:id]) users = User.find(:all) users.each do |user| membership = Membership.find(:first, :conditions => ["user_id = ? and event_id = ?", user.id, @event.id]) if ((params[:proposed][user.id.to_s] == "yes") || (params[:joined][user.id.to_s] == "yes")) if (! membership) membership = Membership.new membership.event_id = @event.id membership.user_id = user.id end if (params[:joined][user.id.to_s] == "yes") membership.joined = true else membership.joined = false end membership.save else if (membership) membership.destroy end end end
eventが削除されたら関連するmembershipも削除する。
def destroy @event = Event.find(params[:id]) memberships = Membership.find(:all, :conditions => ["event_id = ?", @event.id]) memberships.each do |membership| membership.destroy end @event.destroy redirect_to :action => 'list' end
同様に、userが削除されたら関連するmembershipも削除するよう、 user_controller.rbも修正しておこう。
def destroy @user = User.find(params[:id]) memberships = Membership.find(:all, :conditions => ["user_id = ?", @user.id]) memberships.each do |membership| membership.destroy end @user.destroy redirect_to :action => 'list' end
mm/edit.rhtmlに戻ろう。 models/user.rbで、membershipと参加フラグを返せるようにしておく。
class User < ActiveRecord::Base def memberships mhash = Hash::new m = Membership.find(:all, :conditions => ["user_id = ?", self.id]) m.each do |membership| mhash[membership.event_id] = membership end return mhash end def joins jhash = Hash::new memberships.each do |id, membership| jhash[membership.event_id] = membership.joined end return jhash end
mm/edit.rhtmlで、ログインしているのが管理者ならば管理者専用メニューを表示する。
<% if (@user.root) %> <h2>管理者メニュー</h2> <%= link_to 'ユーザ管理', :controller => 'user' %> <%= link_to 'イベント管理', :controller => 'event' %> <% end %>
一般ユーザも、ニックネーム、メールアドレス、参加フラグは更新出来るように、 text_field, check_boxで表示する。
<% form_tag :action => 'update', :id => @user do %> <h2>アカウント情報</h2> <table> <tr><td>会員番号</td><td><%= @user.accountnumber %></td></tr> <tr><td>ニックネーム</td><td><%= text_field("user", "nickname") %></td></tr> <tr><td>メールアドレス</td><td><%= text_field("user", "mailaddress") %></td></tr> </table> <h2>参加状況</h2> <table> <tr><td>タイトル</td><td>参加</td></tr> <% @memberships.each do |id, membership| %> <tr> <td><%= Event.find(membership.event_id).title %></td> <td><%= check_box("join", membership.event_id, {:checked => @joins[membership.event_id]}, "yes", "no") %></td> </tr> <% end %> </table> <%= submit_tag '更新' %> <% end %> <%= link_to 'ログアウト', :action => 'login' %>
最後はend_form_tagだとなぜかエラーするので、endにする。 落ちつきが悪いのでログアウトへのリンクを付ける。
mm_controller.rbで更新を反映させる。もうちょっと簡潔に書く方法がありそうな感じだけど。
def update @user = User.find(params[:id]) @user.nickname = params[:user][:nickname] @user.mailaddress = params[:user][:mailaddress] @memberships = @user.memberships @memberships.each do |id, membership| if (params[:join][id.to_s] == "yes") membership.joined = true else membership.joined = false end membership.save end @user.save redirect_to(:action => "edit", :id => params[:id]) end
user/editでユーザ情報を表示すると、現在の状態に関らず常にrootでTrueが選択されている。 user/_form.rhtmlで
<p><label for="user_root">Root</label><br/> <select id="user_root" name="user[root]"><option value="false">False</option><option value="true" selected>True</option></select></p>
となっているからのようだ。
user_controller.rbで、現在の状態に従って、片方にselectedを代入する。 def edit @user = User.find(params[:id]) if (@user.root) @true_select = 'selected' @false_select = '' else @true_select = '' @false_select = 'selected' end end
変数に従って適当な方がselectedになるようにuser/_form.rhtmlを変更。
<option value="false" <%= @false_select %>>False</option><option value="true" <%= @true_select %>>True</option>
両方ともnilで呼ばれることもありそうだが、気にしない。