ラベル Rails の投稿を表示しています。 すべての投稿を表示
ラベル Rails の投稿を表示しています。 すべての投稿を表示

2021年6月25日金曜日

Ruby on Railsにおける多対多モデルの実装

学生が作らんとしているアプリ,入門書ではなかなか触れられていない際どいところに触れてきよったので,補講をした.以下はその記録から要点を抜粋したものである.

やりたいこと

学生たちは自分たちが授業評価をするアプリを作りたいそうだ.みんなのキャンパス学内版,みたいなものか.それでデータ構造を考えている.一人の学生は複数の授業を履修する.ひとつの授業は複数の学生が履修する.したがって,学生テーブルと授業テーブルを作ると,それは多対多の関係ということになる.

ER図はこんな感じ.

モデルの作成

さて,では必要なモデルを作っていこう.まず,学生モデル(Student)を次の操作で作る.

$ bin/rails g model student name:string sid:string

Running via Spring preloader in process 8711

      invoke  active_record

      create    db/migrate/20210625063116_create_students.rb

      create    app/models/student.rb

      invoke    test_unit

      create      test/models/student_test.rb

      create      test/fixtures/students.yml

$ bin/rails db:migrate

== 20210625063116 CreateStudents: migrating ===================================

-- create_table(:students)

   -> 0.0083s

== 20210625063116 CreateStudents: migrated (0.0085s) ==========================


$ 

次に,授業モデル(Lesson)も次の操作で作る.

$ bin/rails g model lesson name:string teacher:string room:string

Running via Spring preloader in process 8788

      invoke  active_record

      create    db/migrate/20210625063223_create_lessons.rb

      create    app/models/lesson.rb

      invoke    test_unit

      create      test/models/lesson_test.rb

      create      test/fixtures/lessons.yml

$ bin/rails db:migrate

== 20210625063223 CreateLessons: migrating ====================================

-- create_table(:lessons)

   -> 0.0052s

== 20210625063223 CreateLessons: migrated (0.0053s) ===========================


$ 

多対多の関係なので,間を取り持つ学生-授業モデル(StudentLesson)を作る.

これがなぜ必要になるのか,なかなか分かりにくいのだが,順繰りに解き明かしていくことにする.とりあえずは,次の操作で学生テーブルと授業テーブルを参照するモデルである StudentLesson を作る.

$ bin/rails g model student_lesson student:references lesson:references

Running via Spring preloader in process 8906

      invoke  active_record

      create    db/migrate/20210625063559_create_student_lessons.rb

      create    app/models/student_lesson.rb

      invoke    test_unit

      create      test/models/student_lesson_test.rb

      create      test/fixtures/student_lessons.yml

$ bin/rails db:migrate

== 20210625063559 CreateStudentLessons: migrating =============================

-- create_table(:student_lessons)

   -> 0.0133s

== 20210625063559 CreateStudentLessons: migrated (0.0133s) ====================


$

モデルクラスの修正

app/models/student_lesson.rb を見てみると,次のようになっている.

class StudentLesson < ApplicationRecord

  belongs_to :student

  belongs_to :lesson

end

学生と授業の間を取り持つクラスはそれぞれに belongs_to しているので,これはそのままこのとおりにしておけばよい.

app/models/student.rb はどうか.ここには,student_lessons を介してlessonsを持つよということを追記する.具体的には次に示す has_many 句の2行を追加する.

class Student < ApplicationRecord

  has_many :student_lessons

  has_many :lessons, through: :student_lessons

end

app/models/lesson.rb にも同様の修正を加える.

class Lesson < ApplicationRecord

  has_many :student_lessons

  has_many :studentsthrough:student_lessons

end


多対多関係の記述,その効果

このようにしておくと,次の操作が可能になる.ActiveRecord の威力を感じられたい.

まず,Studentをひとつ,作る.これを変数 s に入れる.

$ bin/rails c

Running via Spring preloader in process 9093

Loading development environment (Rails 6.1.4)

irb(main):001:0> s = Student.create(name: 'Jun IIO'sid: '21G0123456J')

  TRANSACTION (0.1ms)  BEGIN

  Student Create (5.7ms)  INSERT INTO "students" ("name", "sid", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["name", "Jun IIO"], ["sid", "21G0123456J"], ["created_at", "2021-06-25 06:44:17.699274"], ["updated_at", "2021-06-25 06:44:17.699274"]]

  TRANSACTION (6.1ms)  COMMIT

=> 

#<Student:0x00000001071e0d18

...

次に,Lessonもひとつ,作っておこう.これは変数 l に入れておく.

irb(main):002:0> l = Lesson.create(name: 'Programming I'teacher: 'Taro YAMADA'room: '301')

  TRANSACTION (0.1ms)  BEGIN

  Lesson Create (2.1ms)  INSERT INTO "lessons" ("name", "teacher", "room", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id"  [["name", "Programming I"], ["teacher", "Taro YAMADA"], ["room", "301"], ["created_at", "2021-06-25 06:44:56.034435"], ["updated_at", "2021-06-25 06:44:56.034435"]]

  TRANSACTION (0.3ms)  COMMIT

=> 

#<Lesson:0x0000000107228050

...

このようにしておくと,s.lessons << l という操作で,s に l を関連付けることができる(sに紐づけられた lessons に l を追加するという操作である).学生 s が授業 l を受講している状態を表していると解釈することができる.この操作で,StudentLesson のエントリが1つ作られていることがおわかりだろうか?(「StudentLesson Create」とログが出ている)

irb(main):003:0> s.lessons << l

  TRANSACTION (0.2ms)  BEGIN

  StudentLesson Create (6.0ms)  INSERT INTO "student_lessons" ("student_id", "lesson_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["student_id", 1], ["lesson_id", 1], ["created_at", "2021-06-25 06:45:31.907370"], ["updated_at", "2021-06-25 06:45:31.907370"]]

  TRANSACTION (0.3ms)  COMMIT

  Lesson Load (0.3ms)  SELECT "lessons".* FROM "lessons" INNER JOIN "student_lessons" ON "lessons"."id" = "student_lessons"."lesson_id" WHERE "student_lessons"."student_id" = $1  [["student_id", 1]]

=> 

[#<Lesson:0x0000000107228050

  id: 1,

  name: "Programming I",

  teacher: "Taro YAMADA",

  room: "301",

  created_at: Fri, 25 Jun 2021 06:44:56.034435000 UTC +00:00,

  updated_at: Fri, 25 Jun 2021 06:44:56.034435000 UTC +00:00>]

わざわざ中間的な関連を示す中間テーブルを用意した威力は,次の操作で確認することができる.すなわち,今の操作「だけ」で授業に参加する学生の一覧も取ることができるようになるということである.

l.students を表示させてみよう.これは,授業 l を受講している学生の一覧を取得する,という操作である.これまで,授業 l に関しては何の操作もしていない.しかし,次のように受講者の一覧(といってもまだ1人だけだが)を自動的に得ることを,確認できるだろう.

irb(main):005:0> l.students

  Student Load (0.4ms)  SELECT "students".* FROM "students" INNER JOIN "student_lessons" ON "students"."id" = "student_lessons"."student_id" WHERE "student_lessons"."lesson_id" = $1  [["lesson_id", 1]]

=> 

[#<Student:0x000000010702c850

  id: 1,

  name: "Jun IIO",

  sid: "21G0123456J",

  created_at: Fri, 25 Jun 2021 06:44:17.699274000 UTC +00:00,

  updated_at: Fri, 25 Jun 2021 06:44:17.699274000 UTC +00:00>]

irb(main):006:0>

2021年6月14日月曜日

Bootstrap5のRails6への導入方法

「最短距離でしっかり身に付く!Webアプリケーション開発の教科書 〜Ruby on Railsで作る本格Webアプリ〜」ではRailsにBootstrapを導入する手順を説明している.執筆当時,Bootstrapのバージョンは4だったのだが,いつの間にかバージョンアップしていて,書籍で説明している手順ではうまく適用できなくなっている.

そこで,ここではその補足として,現時点(2021年6月)でのBootstrap導入方法について説明する.

Bootstrap5の導入

まず,端末から次の操作をしよう.

$ yarn add bootstrap

$ yarn add @popperjs/core

次に,app/views/layouts/application.html.erb のヘッダー部を,次のように修正する.具体的には,stylesheet_pack_tag の行(赤字部分)を追加する.

<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>

<%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>

<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>

app/javascript/stylesheets/application.scss を,次の手順で用意しよう.

$ mkdir app/javascript/stylesheets

$ echo '@import "bootstrap";' > app/javascript/stylesheets/application.scss

app/javascript/stylesheets/application.scss には,以下の内容が書き込まれているはずだ.

@import "bootstrap";

さらに,app/javascript/packs/application.js に次を追記する.

import "bootstrap"

import "../stylesheets/application"

書き込む場所は,import "channels" と Rails.start() の間でよい.

テスト

rails new した後に,上記手順でBootstrapを導入したとする.テストのために,コントローラとビューを1つ作る.

$ bin/rails g controller home index

app/views/home/index.html.erb に,以下の内容を追記しよう.

<button type="button" class="btn btn-primary">Primary</button>

<button type="button" class="btn btn-secondary">Secondary</button>

<button type="button" class="btn btn-success">Success</button>

<button type="button" class="btn btn-danger">Danger</button>

<button type="button" class="btn btn-warning">Warning</button>

<button type="button" class="btn btn-info">Info</button>

<button type="button" class="btn btn-light">Light</button>

<button type="button" class="btn btn-dark">Dark</button>

追記する場所はどこでも構わないが,まあ,末尾でよい.

$ bin/rails s

サーバを起動して,ブラウザで,http://localhost:3000/home/index をアクセスしてみよう.正しく設定できていれば,次のような画面になるはずだ.Bootstrap化されていることを確認できるだろう.

2020年5月18日月曜日

簡易SNSを作ってみよう(16)

管理画面の作成
現在のバージョンでは,データの管理にはコンソールから操作するか,データベースを直接いじらなければならない.不適切な投稿があったときに削除できるように,管理画面を用意する.
Administrateのインストール
管理画面を簡単に作ってくれるgemであるAdministrateをインストールする.以下の手順でルーティングの設定まで自動でやってくれる.なお,そのままだとエラーが出るため,設定ファイルに若干の追記を行う.
【手順】
vi Gemfile
(以下を追記)
gem 'administrate'

bundle install
bin/rails g administrate:install
vi config/initializers/assets.rb
(以下を追記)
Rails.application.config.assets.precompile += 
    %w( administrate/application.js administrate/application.css )

bin/rails assets:precompile
Administrateのテスト
サーバを起動し,http://localhost:3000/adminにアクセスする.管理画面が出てきたらOK.

アクセスコントロール
このままだと誰でも管理画面にアクセスできてしまうので,下記の修正を行い(とりあえずは)先生だけが管理画面にアクセスできるようにする.

【手順】
vi app/controllers/admin/application_controller.rb
(以下を追記)
module Admin
  class ApplicationController < 
Administrate::ApplicationController
    before_action :authenticate_admin

    def authenticate_admin
      # TODO Add authentication logic here.
      redirect_to '/', alert: 'Not authorized.' \
        unless current_user && current_user.role == 'teacher'
    end

    # Override this value to ...
    # on index pages. Defaults to 20.
    # def records_per_page
    #   params[:per_page] || 20
    # end
  end
end

管理画面のカスタマイズ
このままでは管理画面が見づらいので,ちょっとしたカスタマイズを行い見やすくする.
【手順】

vi app/dashboards/user_dashboard.rb
(以下を修正)
class UserDashboard < Administrate::BaseDashboard
  # ATTRIBUTE_TYPES
  # a hash that describes the type of each of the model's fields.
  #
  # Each different type represents an Administrate::Field object,
  # which determines how the attribute is displayed
  # on pages throughout the dashboard.
  ATTRIBUTE_TYPES = {
    posts: Field::HasMany,
    visits: Field::HasMany,
    id: Field::Number,
    #email: Field::String,
    #encrypted_password: Field::String,
    #reset_password_token: Field::String,
    #reset_password_sent_at: Field::DateTime,
    #remember_created_at: Field::DateTime,
    #created_at: Field::DateTime,
    #updated_at: Field::DateTime,
    username: Field::String,
    fullname: Field::String,
    password: Field::Password,
    role: Field::String,
  }.freeze

  # COLLECTION_ATTRIBUTES
  # ...
  # Feel free to add, remove, or rearrange items.
  COLLECTION_ATTRIBUTES = %i[
  id
  username
  fullname
  posts
  ].freeze

  # SHOW_PAGE_ATTRIBUTES
  # an array ....
  SHOW_PAGE_ATTRIBUTES = %i[
  id
  username
  fullname
  role
  ].freeze

  # FORM_ATTRIBUTES
  # an array of attributes that will be displayed
  # on the model's form (`new` and `edit`) pages.
  FORM_ATTRIBUTES = %i[
  username
  fullname
  password
  role
  ].freeze

ページネーションの表示が崩れているのでそれを修正する.Bootstrap4対応のKaminariではなくdefaultのKaminariファイルを必要とするので,quick hackではあるが一度そのファイルを生成してAdministrate用に置いたあと,再度,Bootstrap4に合わせたページネーション用ファイルを生成するということをやる.
【手順】

bin/rails g administrate:views:index
vi app/views/admin/application/index.html.erb
(以下を追加)
<%= paginate resources, views_prefix: 'admin' %>

bin/rails g kaminari:views default -f
mkdir app/views/admin/kaminari
mv app/views/kaminari/* app/views/admin/kaminari/
bin/rails g kaminari:views bootstrap4

Administrateのテスト(2)
サーバを起動し,http://localhost:3000/admin にアクセスする.user000でログインしたとき「のみ」管理画面にアクセスできることを確認する.

2020年5月13日水曜日

簡易SNSを作ってみよう(15)

データダウンロード用APIの作成
隠し機能として,データダウンロードAPIを用意しておく.ネームスペースとしてAPIという別空間を用意しておき,そのなかでデータダウンロード用の手続きを実装する.

【手順】
vi config/routes.rb
(以下を追加)
Rails.application.routes.draw do

…(略)…
  namespace :api, { format: 'json' } do
    resources :users, :only => [:show]
  end
end

mkdir app/controllers/api
vi app/controllers/api/users_controller.rb
(以下を作成)
class Api::UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
    ary = [@user]
    ary.push(@user.posts)
    render json: ary
  end
end

このようにしておくことで,ユーザーごとの投稿データをJSON形式でダウンロードできるようになる.アクセスは,http://localhost:3000/api/users/{id}でよい.ブラウザからアクセスして確認してみること.
本来はデータ保護のため,ログインしないとアクセスできないようにしておくべき(before_ action authenticate_user! をきちんと書いておく)だが,プログラムからの一括ダウンロードを可能にしたいので,とりあえずはこのままにしておく.