学生向けプログラミング入門 | 無料

学生向けにプログラミングを無料で解説。Java、C++、Ruby、PHP、データベース、Ruby on Rails, Python, Django

Rails7.1 | 動画学習アプリ作成 | 30 | サブスクリプション

↓↓クリックして頂けると励みになります。



29 | Wistia】 << 【ホーム】 >> 【31 | 購入プロジェクト表示


購入した人のみがタスクを閲覧できる仕組み(サブスクリプション)を実装していきます。

モデル

サブスクリプションモデルを作成します。


コマンド
rails g model Subscription project:references user:references


「db\migrate\20200804060608_create_subscriptions.rb」ファイルを編集します。


記述追加 db\migrate\20200804060608_create_subscriptions.rb
9行目に「add_index :subscriptions, [:project_id, :user_id], unique: true」の記述を追加しています。

class CreateSubscriptions < ActiveRecord::Migration[7.1]
  def change
    create_table :subscriptions do |t|
      t.references :project, null: false, foreign_key: true
      t.references :user, null: false, foreign_key: true

      t.timestamps
    end
    add_index :subscriptions, [:project_id, :user_id], unique: true
  end
end



コマンド マイグレーション適用
rails db:migrate


「app\models\project.rb」ファイルに以下の記述を追加します。


記述追加 app\models\project.rb(5,6行目)

    has_many :subscriptions
    has_many :users, through: :subscriptions



app\models\project.rb

class Project < ApplicationRecord

  belongs_to :user
  has_many :tasks
  has_many :subscriptions
  has_many :users, through: :subscriptions

  has_rich_text :description
  has_many_attached :images

  validates :name, presence: true, length: { maximum: 50 }
  validates :description, presence: true, length: { maximum: 1000 }
  validates :price, presence: true, numericality: { only_integer: true }
end



「app\models\user.rb」ファイルに以下の記述を追加します。


記述追加 app\models\user.rb(4,5行目)

   has_many :subscriptions
   has_many :projects, through: :subscriptions



app\models\user.rb

class User < ApplicationRecord

  has_many :projects
  has_many :subscriptions
  has_many :projects, through: :subscriptions

  has_one_attached :avatar

  validates :full_name, presence: true, length: {maximum: 50}
  
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
          :confirmable, :omniauthable

  def self.from_omniauth(auth)
    user = User.where(email: auth.info.email).first

    if user
      return user
    else
      where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
        user.email = auth.info.email
        user.password = Devise.friendly_token[0, 20]
        user.full_name = auth.info.name   # ユーザーモデルに名前があると仮定
        user.image = auth.info.image # ユーザーモデルに画像があると仮定

        user.uid = auth.uid
        user.provider = auth.provider

      end
    end
  end
end


コントローラー

コントローラを作成していきます。


「app\controllers」フォルダに「charges_controller.rb」ファイルを新規作成します。


app\controllers\charges_controller.rb(新規作成したファイル)

class ChargesController < ApplicationController
	
	before_action :authenticate_user!

	def free
		project = Project.find(params[:project_id])
		current_user.subscriptions.create(project: project)

		redirect_to project
	end
end



「app\controllers\projects_controller.rb」ファイルの編集をします。


1.記述追加 app\controllers\projects_controller.rb(37行目)
「show()」メソッドの記述を追加しています。

  def show
  	@project = Project.find(params[:id])
  	@tasks = @project.tasks.order(:tag)
    @i = 0
    @images = @project.images
    @joined = false
    @users = @project.users.order('created_at desc').first(10)

    if !current_user.nil? && !current_user.projects.nil?
      @joined = current_user.projects.include?(@project)
    end    
  end



2.記述追加 app\controllers\projects_controller.rb(66行目)
新たに「list()」メソッドを追加しています。

  def list
    if !current_user.nil?
      @projects = current_user.projects
    end
  end



app\controllers\projects_controller.rb

class ProjectsController < ApplicationController

  protect_from_forgery except: [:upload_photo]
  before_action :authenticate_user!, except: [:show]
  before_action :set_project, except: [:new, :create, :show, :index]
  before_action :is_authorised, only: [:naming, :pricing, :description, :photo_upload, :update]

  def index
  	@projects = Project.all
  end

  def new
    @project = current_user.projects.build
  end

  def create
    @project = current_user.projects.build(project_params)
    if @project.save
      redirect_to naming_project_path(@project), notice: "保存しました"
    else
      redirect_to request.referrer, flash: { error: @project.errors.full_messages }
    end
  end

  def update
    new_params = project_params
    new_params = project_params.merge(active: true) if is_ready_project

    if @project.update(new_params)
      flash[:notice] = "保存しました。"
    else
      flash[:alert] = { error: @project.errors.full_messages }
    end
    redirect_back(fallback_location: request.referer)
  end

  def show
  	@project = Project.find(params[:id])
  	@tasks = @project.tasks.order(:tag)
    @i = 0
    @images = @project.images
    @joined = false
    @users = @project.users.order('created_at desc').first(10)

    if !current_user.nil? && !current_user.projects.nil?
      @joined = current_user.projects.include?(@project)
    end    
  end

  def edit
    @project = Project.find(params[:id])
  	@tasks = @project.tasks.order(:tag)
  end

  def upload_photo
    @project.images.attach(params[:file])
    render json: { success: true }
  end

  def delete_photo
    @image = ActiveStorage::Attachment.find(params[:photo_id])
    @image.purge
    redirect_to photo_upload_project_path(@project)
  end

  def list
    if !current_user.nil?
      @projects = current_user.projects
    end
  end

  private
  def set_project
    @project = Project.find(params[:id])
  end

  def project_params
    params.require(:project).permit(:name, :content, :price, :description, :images, :active)
  end

  def is_authorised
    redirect_to root_path, alert: "権限がありません。" unless current_user.id == @project.user_id
  end
  
  def is_ready_project
    !@project.active && !@project.price.blank? && !@project.name.blank? && !@project.images.blank? && !@project.description.blank?
  end

end



「app\controllers\tasks_controller.rb」ファイルを編集します。


記述変更 app\controllers\tasks_controller.rb
show()メソッドの記述を変更しています。

class TasksController < ApplicationController

  before_action :set_task, except: [:index, :new, :create, :show]
  before_action :authenticate_user!, except: [:show]

  def index
    project = Project.find(params[:project_id])
  	@tasks = project.tasks.order(:tag)
  end

  def new
    @task = Task.new
    @projects = Project.all
  end

  def show
    project = Project.find(params[:project_id])
  	@tasks = project.tasks.order(:tag)

  	joined = false
  	if !current_user.nil? && !current_user.projects.nil?
  		joined = current_user.projects.include?(project)
  	end
  	if joined
	  	@task = @tasks.find(params[:id])
	  	@next_task = @task.next
	  	@prev_task = @task.prev
	  else
	  	flash[:alert] = "プロジェクトを購入して下さい。"
	  	redirect_to project
	  end
  end

  def create
    @task = Task.new(task_params)
    if @task.save
      redirect_to naming_task_path(@task), notice: "保存しました。"
    else
      redirect_to request.referrer, flash: { error: @task.errors.full_messages }
    end    

  end

  def naming
  end

  def description
  end

  def video
    @projects = Project.all
  end

  def update
    new_params = task_params
    if @task.update(new_params)
      flash[:notice] = "保存しました。"
    else
      flash[:alert] = "問題が発生しました。"
    end
    redirect_back(fallback_location: request.referer)
  end

  private
  # コールバックを使用して、アクション間で共通のセットアップまたは制約を共有します。
  def set_task
    @task = Task.find(params[:id])
  end

  # 信頼できるパラメータのリストのみを許可します。
  def task_params
    params.require(:task).permit(:title, :note, :video, :header, :description, :tag, :active, :project_id)
  end

end


ルート

ルートを設定します。


記述追加 config\routes.rb
13行目に「post '/free', to: 'charges#free'」の記述を追加しています。

Rails.application.routes.draw do

  # ルートを app\views\pages\home.html.erb に設定
  root 'pages#home'

  # get
  get 'pages/home'
  get '/dashboard', to: 'users#dashboard'
  get '/users/:id', to: 'users#show', as: 'user'

  # post
  post '/users/edit', to: 'users#update'
  post '/free', to: 'charges#free'

  # device
  devise_for :users, 
    path: '', 
    path_names: {sign_up: 'register', sign_in: 'login', edit: 'profile', sign_out: 'logout'},
    controllers: {omniauth_callbacks: 'omniauth_callbacks', registrations: 'registrations'}

  resources :projects do
    member do
      get 'naming'
      get 'pricing'
      get 'description'
      get 'photo_upload'
      delete :delete_photo
      post :upload_photo
    end
    resources :tasks, only: [:show, :index]
  end

  resources :tasks, except: [:edit] do
    member do
      get 'naming'
      get 'description'
      get 'video'
      get 'code'
    end
  end

  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

  # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
  # Can be used by load balancers and uptime monitors to verify that the app is live.
  get "up" => "rails/health#show", as: :rails_health_check

  # Defines the root path route ("/")
  # root "posts#index"
end


ビュー

ビューを編集します。


「app\views\projects\show.html.erb」ファイルを編集します。


1. 記述追加 app\views\projects\show.html.erb(6行目)

<span class="badge bg-<% if @project.price != 0 %>danger<% else %>success<% end %> fs-6"><%= @project.price == 0? "無料" : "有料" %></span>



2. 記述変更 app\views\projects\show.html.erb(20~33行目)

<!-- サブスクリプション -->
<% if !@joined %>

    <%= form_tag free_path do %>
        <%= hidden_field_tag 'project_id', @project.id %>

        <button type="submit" class="btn btn-danger w-100">購入する (<%= number_to_currency(@project.price) %>)</button>
    <% end %>

<% else %>
    <div class="card-content center">
        <span class="badge bg-success fs-4">購入済みです</span>
    </div>
<% end %>



3. 記述変更 app\views\projects\show.html.erb(53~78行目)

<% if @joined %>
    <div class="alert alert-success font2 fs-5">下記のリンクからタスクを見ることができます。</div>
    <div class="list-group mt-2">
        <% @tasks.each do |task| %>
            <% if task.header %>
                <div class="list-group-item list-group-item-action fs-6 mt-2 bg-dark text-light"><%= task.title %></div>
            <% else %>
                <%= link_to [task.project, task], style: "text-decoration:none;", data: { turbo: false} do %>
                    <div class="list-group-item list-group-item-action ml-2 fs-5"><%= task.title %></div>
                <% end %>
            <% end %>
        <% end %>
    </div>
<% else %>
    <div class="alert alert-danger font2 fs-5">購入するとタスクを見ることができます。</div>
    <div class="list-group mt-2">
        <% @tasks.each do |task| %>
            <% if task.header %>
                <div class="list-group-item list-group-item-action fs-6 mt-2 bg-dark text-light"><%= task.title %></div>
            <% else %>
                <div class="list-group-item list-group-item-action ml-2 fs-5"><%= task.title %></div>
            <% end %>
        <% end %>
    </div>                    

<% end %>



app\views\projects\show.html.erb

<div class="container">
    <div class="row">
        <div class="col-md-4">
            <div class="card mt-4">
                <div class="card-body">
                    <span class="badge bg-<% if @project.price != 0 %>danger<% else %>success<% end %> fs-6"><%= @project.price == 0? "無料" : "有料" %></span>
                    <h4 class="font2"><%= @project.name %></h4>
                    <div><strong>タスク数: <i class="far fa-clock"></i> <%= @tasks.count %></strong></div>

                    <div class="mt-2">
                        <%= link_to user_path(@project.user), style: "text-decoration:none;" do %>
                            <%= image_tag avatar_url(@project.user), class: "bd-placeholder-img figure-img img-fluid rounded-pill", style: "width: 50px;" %>
                            <span class="font2 text-dark h4"><%= @project.user.full_name %></span>
                        <% end %>
                    </div>                    

                    <div class="mt-4">
                        <% if user_signed_in? %>

                            <!-- サブスクリプション -->
                            <% if !@joined %>

                                <%= form_tag free_path do %>
                                    <%= hidden_field_tag 'project_id', @project.id %>

                                    <button type="submit" class="btn btn-danger w-100">購入する (<%= number_to_currency(@project.price) %>)</button>
                                <% end %>

                            <% else %>
                                <div class="card-content center">
                                    <span class="badge bg-success fs-4">購入済みです</span>
                                </div>
                            <% end %>

                        <% else %>
                            <button class="btn btn-danger w-100" disabled>ログインして下さい</button>  
                        <% end %>

                    </div>                 
                </div>
            </div>
        </div>
        <div class="col-md-8">
 
            <div class="card mt-4 mb-4">
                <div class="card-body">
                    <h3 class="font1"><%= @project.name %></h3>
                    <div class="font2">
                        <%= @project.description %>
                    </div>
                    <div class="badge bg-danger fs-5 mb-4 mt-2"><%= number_to_currency(@project.price) %></div>

                    <% if @joined %>
                        <div class="alert alert-success font2 fs-5">下記のリンクからタスクを見ることができます。</div>
                        <div class="list-group mt-2">
                            <% @tasks.each do |task| %>
                                <% if task.header %>
                                    <div class="list-group-item list-group-item-action fs-6 mt-2 bg-dark text-light"><%= task.title %></div>
                                <% else %>
                                    <%= link_to [task.project, task], style: "text-decoration:none;", data: { turbo: false} do %>
                                        <div class="list-group-item list-group-item-action ml-2 fs-5"><%= task.title %></div>
                                    <% end %>
                                <% end %>
                            <% end %>
                        </div>
                    <% else %>
                        <div class="alert alert-danger font2 fs-5">購入するとタスクを見ることができます。</div>
                        <div class="list-group mt-2">
                            <% @tasks.each do |task| %>
                                <% if task.header %>
                                    <div class="list-group-item list-group-item-action fs-6 mt-2 bg-dark text-light"><%= task.title %></div>
                                <% else %>
                                    <div class="list-group-item list-group-item-action ml-2 fs-5"><%= task.title %></div>
                                <% end %>
                            <% end %>
                        </div>                    

                    <% end %>

                </div>
            </div>

        <!-- カルーセル表示 -->
        <div class="card">
            <div class="card-body">
                <div id="carouselExampleIndicators" class="carousel slide" data-bs-ride="carousel">
                    <div class="carousel-indicators">
                        <% @images.each do |image| %>
                            <button type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide-to="<%= @i %>" class="<%= 'active' if image.id == @images[0].id %>" aria-current="true" aria-label="Slide <%= @i+1 %>"></button>
                            <% @i = @i +1 %>
                        <% end %>
                    </div>
                    <div class="carousel-inner">
                        <%  @project.images.each do |image| %>
                            <div class="carousel-item <%= 'active' if image.id == @images[0].id %>">
                                <%= image_tag url_for(image), class: "d-block w-100", style: "border-radius: 10px;" %>
                            </div>
                        <% end %>
                    </div>
                    <button class="carousel-control-prev" type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide="prev">
                        <span class="carousel-control-prev-icon" aria-hidden="true"></span>
                        <span class="visually-hidden">Previous</span>
                    </button>
                    <button class="carousel-control-next" type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide="next">
                        <span class="carousel-control-next-icon" aria-hidden="true"></span>
                        <span class="visually-hidden">Next</span>
                    </button>
                </div>
            </div>
        </div>
    </div>
</div>



ブラウザ確認
http://localhost:3000/projects/3


購入しないとタスクのリンクがないので内容を見ることができません。

購入していない状態
購入していない状態



購入すると、タスク一覧のリンクより内容を見ることができるようになります。

購入済み
購入済み


モバイルレイアウト
モバイルレイアウト



サブスクリプションテーブルを確認します。

サブスクリプションテーブル確認
サブスクリプションテーブル確認



29 | Wistia】 << 【ホーム】 >> 【31 | 購入プロジェクト表示




↓↓クリックして頂けると励みになります。