צעדים ראשונים עם Ruby On Rails

31/07/2018

ביום חמישי בבוקר אעביר וובינר של שעה על הספריה Ruby On Rails: ספריה לפיתוח יישומי Web מלאים. בשעה אני מתכנן לכתוב אתכם Web Application מלא בו משתמשים יוכלו לפרסם לינקים לדברים שאהבו ומשתמשים אחרים יוכלו לעשות "לייק" על הלינקים.

רוצים לבוא? זה קישור להרשמה:

https://www.tocode.co.il/workshops/44

1. התקנת הסביבה על Windows

נתחיל עם התקנת Ruby ו Rails על מכונת חלונות. הצעד הראשון הוא להוריד את Ruby Installer מהקישור:

https://github.com/oneclick/rubyinstaller2/releases/download/rubyinstaller-2.4.4-2/rubyinstaller-devkit-2.4.4-2-x64.exe

לאחר הורדה והתקנה נכנס ל cmd ונפעיל את הפקודות הבאות כדי להתקין ריילס:

gem install rails
gem install bundler

ויש לנו ריילס! אגב לא יזיק להתקין סביבת פיתוח. אפשר להשתמש בכל Text Editor לצורך פיתוח היישום אבל אני אבחר את Ruby Mine שהיא סביבה קצת יותר רצינית עם השלמות אוטומטיות וצבעים וכל הדברים. אפשר להוריד לחודש ניסיון מהאתר שלהם בקישור:

https://www.jetbrains.com/ruby/download/

2. יצירת פרויקט ראשון

זה כל מה שהיה צריך כדי להתקין ריילס. עכשיו ניצור תיקיה חדשה ובה הפרויקט הראשון שלנו. בשורת הפקודה נרשום:

mkdir demoproject
cd demoproject
rails new webinar-demo

עכשיו אפשר להיכנס ל Ruby Mine ולפתוח את תיקיית הפרויקט החדש שיצרנו כדי להתחיל לעבוד. כדאי גם להשאיר את שורת הפקודה פתוחה כי תכף נצטרך אותה שוב.

3. יצירת Scaffold עבור "לינק"

משתמשים במערכת שלנו יוכלו לפרסם לינקים. בינתיים עוד אין לנו משתמשים ולכן עד שיהיו נתחיל עם טבלא של לינקים ונפרסם את הלינקים בעצמנו עבור כולם. כתבו את הפקודה הבאה מתוך שורת הפקודה:

rails g scaffold link name:string header:string likes:integer

בתגובה המערכת תייצר מסכים ליצירת לינקים חדשים, לעריכת לינקים, למחיקת לינקים וכמובן להצגת כל רשימת הלינקים במערכת. מוזמנים להיכנס לקבצים לראות מה יש בכל אחד מהם.

4. הצגת דף בית עם המוצרים הפופולריים ביותר

ניצור מספר "לינקים" דרך מסכי הניהול ונעבור ליצור דף בית חדש שיציג את 10 הלינקים הפופולריים ביותר במערכת. בשביל זה נצטרך ליצור Controller חדש עבור דף הבית ואיתו View ו Layout לדף זה. הפעילו את הפקודה:

rails g controller home homepage

היכנסו לקובץ app/controllers/home_controller.rb ועדכנו את תוכנו כך שיכיל את הקוד הבא:

class HomeController < ApplicationController
  def homepage
    @links = Link.all.order(likes: :desc).limit(10)
  end
end

תפקיד ה Controller בריילס לקרוא בסיס הנתונים את המידע שמעניין אותנו כדי שיהיה קל להציג אותו. כל מידע שנשמר במשתנה שמתחיל בסימן @ יהיה זמין לשכבת התצוגה לצורך הצגה.

נעבור לקובץ app/views/home/homepage.html.erb ונכתוב בו את התוכן הבא:

<h1>Linker Links</h1>
<ul class="links-list">
  <% @links.each do |link| %>
    <li>
      <%= link_to link.header, link.href, target: '_blank' %> [<%= link.likes %>]
      <button class="btn-like">Like</button>
      <button class="btn-unlike">Unlike</button>
    </li>
  <% end %>
</ul>

קוד זה יגרום להצגת כל הקישורים בתור רשימה. ואפשר גם להוסיף עיצוב - נעבור לקובץ app/assets/stylesheets/home.scss ונוכל להוסיף שם קוד scss שישפיע על עיצוב העמוד:

// Place all the styles related to the links controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

.links-list {
  list-style: none;
  padding: 0;
  li {
    margin: 30px 10px;
    font-size: 18px;
  }
}

body {
  background: beige;
}

5. הוספת משתמשים

הספריה Devise של ריילס מטפלת בכל נושא המשתמשים במערכת. היא מגיעה עם Defaults טובים ומאפשרת התאמה אישית של כל המסכים וכל ההתנהגות. כדי להתקין את הספריה נלך לפי המדריך שכאן:

https://github.com/plataformatec/devise

בסיום ההתקנה קיבלנו טבלת משתמשים בבסיס הנתונים, מסכי התחברות וכניסה ואפשרות להשתמש בנתוני המשתמש הנוכחי במערכת שלנו.

6. עדכון מסכי הלינקים כך שכל משתמש יוכל לערוך רק את הלינקים שלו

בשביל שיהיה מעניין בואו נעדכן את מסכי עריכת הלינקים שקיבלנו כך שכל משתמש יוכל לערוך רק את הלינקים שלו. בשביל זה נצטרך לחבר בין לינק למשתמש באמצעות הוספת שדה user_id לטבלת הלינקים.

הפקודה:

rails g migration add_userid_to_links

תיצור לכם קובץ חדש שנקרא migration. תפקידו של קובץ זה לתאר את השינוי המיוחל בבסיס הנתונים. נכתוב בקובץ את התוכן הבא:

class AddUseridToLink < ActiveRecord::Migration[5.2]
  def change
    add_reference :links, :user
  end
end

נריץ את ה Migration עם הפקודה:

rails db:migrate

ואפשר לראות שבסיס הנתונים עודכן ועכשיו לטבלא links נוספה עמודה user_id.

נמשיך לעדכון המודל. המודל הוא המקום בו ריילס שומר את כל ה Business logic של האפליקציה ואת כל הקשרים בין הטבלאות. שינוי המודל יהפוך את הקוד שמשתמש בנתונים של לינקים ומשתמשים לקל יותר, ובאופן כללי המודל הוא המקום בו נרצה ליצור את רוב הלוגיקה החדשה של התוכנית שלנו.

פתחו את הקובץ app/models/link ורשמו בו את התוכן הבא:

class Link < ApplicationRecord
  belongs_to :user
end

שורה זו תעזור לנו להגיע מ Link למשתמש שמתאים לו. עכשיו נעבור למודל המשתמש ונוסיף שם את השורה:

has_many :links

עכשיו אפשר להמשיך לעדכן את הקובץ app/controllers/links_controller.rb כך שכל פעם שנוצר לינק הוא יווצר עם עמודת ה user_id הנכונה. זה התוכן החדש של הקובץ:

class LinksController < ApplicationController
  before_action :set_link, only: [:show, :edit, :update, :destroy]

  # GET /links
  # GET /links.json
  def index
    @links = current_user.links
  end

  # GET /links/1
  # GET /links/1.json
  def show
  end

  # GET /links/new
  def new
    @link = Link.new(user: current_user)
  end

  # GET /links/1/edit
  def edit
  end

  # POST /links
  # POST /links.json
  def create
    @link = Link.new(link_params)

    respond_to do |format|
      if @link.save
        format.html { redirect_to @link, notice: 'Link was successfully created.' }
        format.json { render :show, status: :created, location: @link }
      else
        format.html { render :new }
        format.json { render json: @link.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /links/1
  # PATCH/PUT /links/1.json
  def update
    respond_to do |format|
      if @link.update(link_params)
        format.html { redirect_to @link, notice: 'Link was successfully updated.' }
        format.json { render :show, status: :ok, location: @link }
      else
        format.html { render :edit }
        format.json { render json: @link.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /links/1
  # DELETE /links/1.json
  def destroy
    @link.destroy
    respond_to do |format|
      format.html { redirect_to links_url, notice: 'Link was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_link
      @link = current_user.links.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def link_params
      params.require(:link).permit(:header, :href, :likes).merge(user: current_user)
    end
end

7. מתן אפשרות לכל משתמש לעשות Like יחיד לקישור

משתמשים במערכת שלנו יכולים לעדכן רק את הקישורים של עצמם אבל הם יכולים גם לעשות לייק לקישורים של אחרים. נרצה שכל משתמש יוכל לעשות לייק רק פעם אחת לכל קישור ולכן נוסיף טבלא חדשה של לייקים המקשרת בין משתמשים לקישורים.

גם יצירת טבלא מתבצעת באמצעות Migration ולכן נכתוב את הפקודה:

rails g model like user:reference link:reference

עדכנו את קובץ ה Migration שנוצר והוסיפו לו אינדקס:

class CreateLikes < ActiveRecord::Migration[5.2]
  def change
    create_table :likes do |t|
      t.references :user
      t.references :link
      t.index [:user_id, :link_id], unique: true

      t.timestamps
    end
    remove_column :links, :likes, :integer
  end
end

ולאחר מכן הפעילו באמצעות:

rails db:migrate

עכשיו אפשר לעבור לקונטרולר ולהוסיף נתיבים כדי לאפשר למשתמש לעשות Like או Unlike לקישור. הכנסו לקובץ app/controllers/home_controller.rb וכתבו בו את הקוד הבא:

class HomeController < ApplicationController
  def homepage
    @links = Link.all.order(likes: :desc).limit(10)
  end

  def like
    link = Link.find(params[:link_id])
    Like.create!(user: current_user, link: link)
    render :homepage
  end

  def unlike
    link = Link.find(params[:link_id])
    like = Like.find_by(user: current_user, link: link)
    like.destroy
    render :homepage
  end
end

בשביל שמשתמשים גם יוכלו לגשת לנתיבים אלו נרצה להוסיף עבורם routes. הקובץ config/routes.rb של ריילס כולל רשימה של כל הנתיבים או כל נקודות הכניסה ליישום שלנו. עדכנו את הקובץ עם הקוד הבא:

Rails.application.routes.draw do
  devise_for :users
  get 'home/homepage'
  resources :links

  post 'like', to: 'home#like', via: :post
  post 'unlike', to: 'home#unlike', via: :post
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end

הוספת הנתיבים מאפשרת לנו לגשת לפעולות שהוגדרו בקונטרולר דרך לחיצה על הכפתורים.

עכשיו נעבור לעדכן את התבנית כדי שהכפתורים יגרמו להפעלת הנתיבים. עדכנו את הקובץ app/views_home/homepage.html.erb עם התוכן הבא:

<h1>Linker Links</h1>
<ul class="links-list">
  <% @links.each do |link| %>
    <li>
      <%= link_to link.header, link.href, target: '_blank' %> [<%= link.likes_count %>]
      <%= link_to 'Like', like_path(link_id: link.id), method: 'POST' %>
      <%= link_to 'Unlike', unlike_path(link_id: link.id), method: 'POST' %>
    </li>
  <% end %>
</ul>

המעבר מספירת הלייקים בתור עמודה בטבלת links לספירת לייקים בטבלא נפרדת כולל גם בעיה: חישוב מספר הלייקים הוא עכשיו יותר מורכב מפשוט לקרוא את השדה likes. נכנס למודל Link ונכתוב בו את הקוד הבא שיעזור לנו בחישוב:

class Link < ApplicationRecord
  belongs_to :user
  has_many :likes, dependent: :destroy

  scope :with_likes_count, -> { Link.left_joins(:likes).select('COUNT(likes.id) as likes_count, links.header, links.href, links.id').group(:id) }
end

הקוד מוסיף מתודה סטטית חדשה לאוביקט Link המחזירה תוצאת Join בין כמה טבלאות. ה join הזה הוא שנותן לנו את ספירת הלייקים לכל שורה.

נחזור לקובץ app/controllers/home_controller.rb ונעדכן אותו כך שיכיל התיחסות למנגנון הספירה החדש:

class HomeController < ApplicationController
  before_action :prepare_links, only: [:homepage, :like, :unlike]

  def homepage
  end

  def like
    link = Link.find(params[:link_id])
    Like.create!(user: current_user, link: link)
    render :homepage
  end

  def unlike
    link = Link.find(params[:link_id])
    like = Like.find_by(user: current_user, link: link)
    like.destroy
    render :homepage
  end

  def prepare_links
    @links = Link.with_likes_count.order('likes_count desc').limit(10)
  end
end

עכשיו אפשר להריץ ולראות את הרשימה וגם לעשות לייק כדי להעלות קישור למקום גבוה יותר.

8. הוספת תמונה לקישור

לריילס יש תמיכה מובנית בקבצים מצורפים והוא מנהל אותם באמצעות שתי הטבלאות active_storage_attachments ו active_storage_blobs. כדי להתחיל להשתמש בטבלאות אלו נריץ:

rails active_storage:install
rails db:migrate

עכשיו אפשר להוסיף גם תמונות לקישורים שלנו! ניכנס למודל Link ונעדכן את הקוד:

# app/models/link.rb

class Link < ApplicationRecord
  belongs_to :user
  has_many :likes, dependent: :destroy

  scope :with_likes_count, -> { Link.left_joins(:likes).select('COUNT(likes.id) as likes_count, links.header, links.href, links.id').group(:id) }

  has_one_attached :thumbnail
end

נמשיך לטופס בקובץ app/views/links/_form.html.erb ונעדכן את הקוד:

<%= form_with(model: link, local: true) do |form| %>
  <% if link.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(link.errors.count, "error") %> prohibited this link from being saved:</h2>

      <ul>
      <% link.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :header %>
    <%= form.text_field :header %>
  </div>

  <div class="field">
    <%= form.label :href %>
    <%= form.text_field :href %>
  </div>

  <div class="field">
    <%= form.label :thumbmail %>
    <%= form.file_field :thumbnail %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

ונסיים עם הקובץ app/controllers/links_controller.rb ונעדכן שם את הפונקציה link_params:

    def link_params
      params.require(:link).permit(:header, :href, :thumbnail).merge(user: current_user)
    end

לסיום נלך ל homepage.html.erb ונוסיף את התמונה שלנו:

<h1>Linker Links</h1>
<ul class="links-list">
  <% @links.each do |link| %>
    <li>
      <% if link.thumbnail.attached? %>
        <%= image_tag link.thumbnail, class: 'thumbnail', width: 80 %>
      <% end %>
      <%= link_to link.header, link.href, target: '_blank' %> [<%= link.likes_count %>]
      <%= link_to 'Like', like_path(link_id: link.id), method: 'POST' %>
      <%= link_to 'Unlike', unlike_path(link_id: link.id), method: 'POST' %>
    </li>
  <% end %>
</ul>

ויש לנו תוספת של תמונות ללינקים.

לסיכום - בלא הרבה מאמץ ובלי הרבה קוד הצלחנו לבנות מערכת שכוללת אחסון קבצים, ניהול משתמשים וכמובן שומרת מידע בבסיס הנתונים והקוד יצא מאוד קריא ונקי. אין ספק שריילס מגיעה כבר עם הסוללות בפנים וכל מה שאנחנו צריכים לעשות זה למלא את התוכן שרלוונטי למערכת שלנו. יחד עם זאת כמו שבטח התרשמתם יש לריילס המון יכולות ויכול לקחת זמן ללמוד את הכל.

כל הקוד שכתבנו זמין בגיטהאב בקישור:

https://github.com/ynonp/rails-webinar-demo-1