↓↓クリックして頂けると励みになります。
【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 | 購入プロジェクト表示】
↓↓クリックして頂けると励みになります。