Пишем социальную сеть на Ruby on Rails. Часть 1

от автора

Кадр из фильма "Социальная Сеть"
Кадр из фильма «Социальная Сеть»

Зачем и для кого эта статья?

Всем привет! Я Ruby on Rails Developer и еще совсем недавно я начинал свой путь в этой области. Я уже прошел первые шаги (о них я писал в данной статье), как выбор языка, изучение его основ, знакомство с фреймворком, первые pet-проекты, первые собеседования, первый оффер, первая компания. Но многие только начали идти по этому пути и именно для них эта статья. По своему опыту помню, как сложно искать гайды (большинство из них про создание книжных магазинов, личных блогов и т.д.), поэтому, надеюсь, многим понравиться идея создания соц сети.

Почему соц сеть и как будет идти процесс?

Во-первых, мне самому было бы интересно реализация данного проекта. Во-вторых, я вдохновился книгой Practical Rails Social Networking Sites by Alan Bradburne. Можно было бы сделать все по книге, скажете вы. Но зачем тогда я и мои статьи? Книга 2007 года и версия ruby там 1.8, поэтому решения в большинстве своем будут неактуальны в наше время. К тому же, я не собираюсь делать все по книге, а лишь руководствоваться ей (дизайн буду использовать из нее с добавлением bootsrap). Во время разработки я буду использовать многие гемы, которые будут полезны для начинающих разработчиков. Но скажу сразу: эта серия статей не для самого старта. Некоторые базовые вещи (установка ruby, rails, что такое MVC, git и тому подобное) я буду пропускать, поэтому рекомендую отложить данный цикл статей и вернуться к нему чуть позже. Если вы опытный разработчик и читаете эту статью, буду очень признателен услышать ваше мнение. Касательно периодичности выхода статей и сколько именно их будет не могу сказать, так как планирую делать проект в свободное время от работы и параллельно писать статьи. Но буду стараться не откладывать в долгий ящик и делать все с хорошим темпом.

Создаем проект и делаем первичные настройки

Перед тем, как мы начнем, установите следующие версии:

  • Ruby 3.0.3

  • Rails 6.1.4.6

  • MySQL 8.0

  • Node 10.19

  • Yarn 1.22.17

Очень советую вам использовать github во время разработки данного проекта. Чуть позже мы с вами настроим CI/CD на нашем проекте, что будет крайне полезным для вас опытом. Теперь создадим наш проект. Я назову его g_connect, но вы можете использовать любое другое название (если выберите другое, везде, где я буду использовать g_connect, пишите свое).

rails new g_connect -d mysql

Теперь переходим в папку с проектом и займемся первичными настройками. Я всегда начинаю с Gemfile и некоторые гемы, которые точно буду использовать во время разработки.

#Gemfile  source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" }  ruby '3.0.3'  gem 'aasm' gem 'bootsnap', '>= 1.4.4', require: false gem 'bootstrap' gem 'devise' gem 'jbuilder', '~> 2.7' gem 'mysql2', '~> 0.5.2' gem 'puma', '~> 5.0' gem 'rails', '~> 6.1.4.6' gem 'sass-rails', '>= 6' gem 'slim' gem 'turbolinks', '~> 5' gem 'webpacker', '~> 5.0'  group :development, :test do   gem 'better_errors'   gem 'binding_of_caller'   gem 'byebug', platforms: %i[mri mingw x64_mingw]   gem 'factory_bot_rails'   gem 'faker'   gem 'rails-controller-testing'   gem 'rspec-rails'   gem 'rubocop'   gem 'rubocop-rails'   gem 'rubocop-rspec' end  group :development do   gem 'annotate'   gem 'listen', '~> 3.3'   gem 'rack-mini-profiler', '~> 2.0'   gem 'spring'   gem 'web-console', '>= 4.1.0' end  group :test do   gem 'capybara', '>= 3.26'   gem 'rspec_junit_formatter'   gem 'selenium-webdriver'   gem 'simplecov', require: false   gem 'webdrivers' end  gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] 

Давайте я поясню, для чего некоторые из этих гемов (более подробно рекомендую перед стартом ознакомиться с документацией):

  • gem ‘aasm’ — его мы будем использовать для транзакций между состояниями

  • gem ‘bootstrap’ — это нам нужно будет для нашего дизайна (я делаю упор на бэк и совсем немного буду уделять время фронту, но в самом конце может и придет вдохновение на наведение красоты)

  • gem ‘devise’ — аутентификация (коротко и ясно, будет еще гем для авторизации, но я еще не выбрал, какой)

  • gem ‘slim’ — сэкономим время на написании html-тегов

  • gem ‘better_errors’ — красивый вывод ошибок в браузере (например, роут указали неправильный)

  • gem ‘factory_bot_rails’ — шаблон, который будем использовать в наших тестах

  • gem ‘faker’ — для создания фейковых данных

  • gem ‘rspec-rails — это подключаем среду тестирования RSpec для нашего проекта

  • gem ‘rubocop’ — проверяем, насколько хорошо написан наш код

  • gem ‘annotate’ — для автоматической аннотации наших моделей (зачем переключаться между моделей и миграцией, если есть этот гем)

  • gem ‘simplecov’ — для просмотра, все ли мы покрыли тестами

Теперь нужно установить все гемы и их зависимости, поэтому запускаем bundle в нашем терминале (кстати да, забыл сказать, что использую Ubuntu, поэтому для MacOS/Windows(вот винду вообще не трогайте лучше при разработке на ruby, но уж если оч хочется) смотрите некоторые моменты самостоятельно). Также можем удалить папку test, она не понадобиться (ведь будем писать rspec-и).

После этого настроим нашу БД. В файле config/database.yml укажите свои username и password (я стандартно делал root/root). После этого запускаем следующее (если кто-то не знает, то эта одна команда сразу же выполняет db:create, db:schema:load и db:seed):

rails db:setup

Некоторые из наших гемов требуют дополнительной настройки. Ими мы сейчас и займемся (devise также требует дополнительной настройки, но им мы займемся позже, когда будем делать аутентификацию). Начнем с bootstrap. Переходим в файл app/assets/stylesheets/application.scss (файл может вначале иметь расширение .css, поэтому исправьте его на .scss) и добавляем следующую строчку:

/*app/assets/stylesheets/application.scss*/  @import "bootstrap"; 

Теперь настроим annotate. Для этого в терминале запустите следующую команду:

rails g annotate:install

Теперь нужно настроить rspec:

rails g rspec:install

Также для наших тестов нам понадобится настройка factory_bot_rails и simplecov. В нашей папке spec создаем папку support, а в ней создаем файл factory_bot.rb со следующим кодом:

#spec/support/factory_bot.rb  RSpec.configure do |config|   config.include FactoryBot::Syntax::Methods end 

Теперь переходим в наш spec/rails_helper.rb. Здесь мы подключим наш файл для factory_bot, а так же подключим simplecov (две строчки для подключения simplecov должны быть в самом начале файла).

#spec/rails_helper.rb  require 'simplecov' SimpleCov.start 'rails' require_relative './support/factory_bot'

С настройкой гемов пока что закончили. Если вы хотите проверить процент покрытия тестами, то можете запустить xdg-open coverage/index.html и вот она магия. Но я бы хотел добавить еще пару моментов. Первый — shared_context.rb для тестов наших моделей. В папке spec/support создайте shared_context.rb

#spec/support/shared_context.rb  RSpec.shared_examples 'creates_object_for' do |model_name|   subject { FactoryBot.build(model_name) }    it 'creates object' do     expect { subject.save }.to change { described_class.count }.by(1)   end end  RSpec.shared_examples 'not_create_object_for' do |model_name, parameter|   subject { described_class.create(attributes) }    let(:attributes) { FactoryBot.attributes_for(model_name, parameter) }    it 'does not create object' do     expect { subject.save }.to change { described_class.count }.by(0)   end    it 'raise RecordInvalid error' do     expect { subject.save! }.to raise_error(ActiveRecord::RecordInvalid)   end end 

Наш shared_context будет играть роль шаблона для тестирования моделей. В нем мы опишем, что должно происходить, если данные валидны и объект создаётся, и наоборот, что будет происходить, если какие-то данные невалидны либо отсутствуют и объект не будет создаваться. Это заметно упростит написание тестов для моделей, потом на практике вы в этом убедитесь. Теперь давайте подключим его в наш spec_helper.rb

#spec/spec_helper.rb  require_relative './support/shared_context' 

И последнее по настройкам перед стартом: чутка дизайна. Перейдите в app/views/layouts/application.html.erb. Измените расширение .erb на .slim и сделайте вот так:

#app/views/layouts/application.html.slim  doctype html html   head     meta content='text/html; charset=UTF-8' http-equiv='Content-Type'     title G-Connect     = csrf_meta_tags     = csp_meta_tag     = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'     = javascript_pack_tag 'application', 'data-turbolinks-track': 'reload'   body     #container       #header                #sidemenu         = render 'application/sidemenu'       #content         = yield 

Как по мне, это намного лучше смотрится, чем .html.erb. Если вы до этого никогда не использовали .slim, то используйте вот этот ресурс для перевода из .html.erb в .html.slim. Далее в папке app/views создаем папку application, а в ней файл _sidemenu.html.slim и внутри него пока что только следующие строчки:

#app/views/layouts/_sidemenu.html.slim  ul   li     = link_to 'Home', '/', class: 'btn btn-sm btn-light' 

Затем переходим в app/assets/stylesheets/application.scss и добавляем следующее:

/*app/assets/stylesheets/application.scss*/  body {    margin: 0;    padding: 0;    background-color: #f0ffff;    font-family: Arial, Helvetica, sans-serif;  }   #header {    background-color: #f0ffff;    height: 60px;    margin-top: 10px;    text-align: left;    padding-top: 1px;  }   #container {    width: 760px;    min-width: 760px;    margin: 0 auto;    padding: 0px;  }   #sidemenu {    font-size: 80%;    float: left;    width: 100px;    padding: 0px;  }   #sidemenu ul {    list-style: none;    margin-left: 0px;    padding: 0px;  }   a {    color: #b00;  }   a:hover {    background-color: #b00; color: #f0ffff;  }   #content {    float: right;    width: 650px;  }   th {    background-color: #933;    color: #f0ffff;  }   tr.odd {    background-color: #fcc;  }   tr.even {    background-color: #ecc;  } 

Пока совсем простенько (css взят из книги), но, как я и говорил ранее, я делаю упор на бэк. Когда первичная работа окончена, можем двигаться дальше. Может залить все, что мы с вами сделали, на github и делать новые вещи уже в другой ветке. Рекомендую перед этим сделать проверку rubocop-ом (можете добавить специальное расширение для вашей IDE и он сразу будет подсвечивать вам файлы, в которых есть недочеты).

Создание модели страниц

Первую модель мы сделаем для страниц. У нее будут следующие поля

Имя поля

Тип

id

integer

title

string

permalink

string

body

text

Permalink будет составляться из нашего title, только будет иметь более красивый вид, чтобы использовать его в качестве url. Давайте создадим модель.

rails g model Page

Помимо модели у нас сгенерировалась миграция и два файла для тестов, к ним мы вернемся чуть позже. Миграция находится в папке db/migrate. Давайте начнем с нее:

#db/migrate/date_time_create_pages.rb  class CreatePages < ActiveRecord::Migration[6.1]   def change     create_table :pages do |t|       t.string :title,          null: false       t.string :permalink       t.text :body,             null: false     end   end end 

После этого запускаем rails db:migrate (наш annotate сразу же показывает, какие файлы были аннотированы) и переходим в нашу модель. Здесь мы пропишем наши валидации, а так же метод для получения нашего permalink (его мы будем вызывать с помощью коллбэка after_create). В этом методе я буду использовать регулярные выражения, для помощи с их составлением я всегда использую Rubular.

#app/model/page.rb  class Page < ApplicationRecord   after_create :clean_url    validates_presence_of :title, :body   validates :title, length: { in: 3..250 }   validates :body, length: { in: 3..100_00 }    private    def clean_url     return unless self.permalink.nil?     url = title.downcase.gsub(/\s+/, '_').gsub(/[^a-zA-Z0-9_]+/, '')     self.permalink = url     save   end end 

Теперь предлагаю заняться нашими тестами. Начнем с файла spec/factories/pages.rb

#spec/factories/pages.rb  FactoryBot.define do   factory :page do     title { 'Test' }     body { 'Test' }   end end 

После этого можем заняться непосредственно написанием тестов. Именно в этом файле нам и пригодится наш написанный ранее spec/supprot/shared_context.rb.

#spec/models/page_spec.rb  require 'rails_helper'  RSpec.describe Page, type: :model do   describe '.create' do     context 'with valid attributes' do       include_examples 'creates_object_for', :page     end      context 'with invalid attributes' do       context 'with short title' do         include_examples 'not_create_object_for', :page, title: 'te'       end        context 'with too long title' do         include_examples 'not_create_object_for', :page, title: Faker::String.random(length: 253)       end        context 'with short body' do         include_examples 'not_create_object_for', :page, body: 'te'       end        context 'with too long body' do         include_examples 'not_create_object_for', :page, body: Faker::String.random(length: 100_02)       end     end      context 'with missing attributes' do       context 'with missing title' do         include_examples 'not_create_object_for', :page, title: nil       end        context 'with missing body' do         include_examples 'not_create_object_for', :page, body: nil       end     end   end end 

Думаю, здесь не нужно никаких пояснений, лишь отмечу, что private методы не нуждаются в тестировании, поэтому здесь и нет тестов для нашего clean_url. Можем запустить rspec в нашем терминале и убедиться, что все наши тесты проходят без ошибок. Когда с моделью и тестами для них мы разобрались, предлагаю заняться контроллером. Я не использую генератор для контроллеров, поэтому создаем файл pages_controller.rb в нашей папке app/controllers. Здесь мы пропишем следующее:

#app/controllers/pages_controller.rb  class PagesController < ApplicationController   before_action :find_page, only: %i[show edit update destroy]    def index     @pages = Page.all   end    def show; end    def new     @page = Page.new   end    def create     @page = Page.create(page_params)      if @page.save       redirect_to pages_path, notice: 'Page created'     else       render :new     end   end    def edit; end    def update     if @page.update(page_params)       redirect_to page_path(@page), notice: 'Page updated'     else       render :edit     end   end    def destroy     @page.destroy     redirect_to pages_path, notice: 'Page deleted'   end    private    def find_page     @page = Page.find(params[:id])   end    def page_params     params.require(:page).permit(:title, :permalink, :body)   end end 

Здесь все с больше абсолютно стандартное, поэтому также не буду заострять на этом внимание. Теперь нужно добавить роуты для нашего контроллера:

#config/routes.rb  Rails.application.routes.draw do   root 'pages#index'    resources :pages end

Теперь давайте покроим тестами наш контроллер. Для это в папке spec создаем папку controllers и там же сразу создаем файл pages_controller_spec.rb

#spec/controllers/pages_controller_spec.rb  require 'rails_helper'  RSpec.describe PagesController, type: :controller do   describe 'GET #index' do     let(:pages) { [FactoryBot.create(:page)] }      it 'returns all pages' do       get :index        expect(response).to render_template('index')       expect(response).to have_http_status(:ok)       expect(assigns(:pages)).to eq(pages)     end   end    describe 'GET #show' do     let(:page) { FactoryBot.create(:page) }      it 'assigns page' do       get :show, params: { id: page.id }        expect(response).to render_template('show')       expect(response).to have_http_status(:ok)       expect(assigns(:page)).to eq(page)     end   end    describe 'GET #new' do     it 'returns render form for creating new page' do       get :new        expect(response).to render_template('new')       expect(response).to have_http_status(:success)     end   end    describe 'POST #create' do     let(:page_params) { FactoryBot.attributes_for(:page) }      it 'creates new page' do       get :create, params: { page: page_params }        expect(response).to redirect_to('/pages')       expect(response).to have_http_status(:found)     end      it 'doesn`t create new page' do       get :create, params: { page: page_params.except(:title) }        expect(response).to render_template('new')     end   end    describe 'PUT #update' do     let(:page) { FactoryBot.create(:page) }      it 'updates the requested page' do       put :update, params: { id: page.id, page: { title: 'brbrbr' } }        expect(response).to redirect_to("/pages/#{page.id}")       expect(response).to have_http_status(:found)     end      it 'doesn`t update page' do       put :update, params: { id: page.id, page: { title: '' } }        expect(response).to render_template('edit')     end   end    describe 'DELETE #destroy' do     let(:page) { FactoryBot.create(:page) }      it 'destroys page' do       delete :destroy, params: { id: page.id }        expect(response).to redirect_to('/pages')     end   end end 

Теперь же предлагаю сделать визуал для нашего контроллера. Переходим в app/views и создаем там папку pages, а в ней 5 файлов: _form.htm.slim, new.html.slim, edit.html.slim, show.html.slim и index.html.slim. Теперь пройдемся по каждому из них. В нашем _form.htm.slim будет находиться форма, которую мы будем заполнять для создания или изменения наших Pages. Эту форму мы будем рендерить в наших new и edit соответственно.

#app/views/pages/_form.html.slim  = form_with(model: page, local: true) do |f|   .form-group     = f.label :title     = f.text_field :title   .form-group     = f.label :body     = f.text_area :body   .form-group     = f.submit 'Submit', class: 'btn btn-success' 
#app/views/pages/new.html.slim  h1 New Page  = render 'form', page: @page  = link_to 'Back', :back, class: 'btn btn-sm btn-primary' 
#app/views/pages/edit.html.slim  h1 Edit Page  = render 'form', page: @page  = link_to 'Back', :back, class: 'btn btn-sm btn-primary' 

Теперь займемся show и index:

#app/views/pages/show.html.slim  p   strong Title:    = @page.title p   strong Body:    = @page.body  = link_to 'Edit', edit_page_path(@page), class: 'btn btn-sm btn-success' ' = link_to 'Delete', page_path(@page), method: :delete, class: 'btn btn-sm btn-danger', data: { confirm: 'Are you sure?' } ' = link_to 'Back', :back, class: 'btn btn-sm btn-primary' 
#app/views/pages/index.html.slim  h2 Pages ul   - @pages.each do |page|     li       = page.permalink       |:        = page.title       |       p           = link_to 'Show', page_path(page), class: 'btn btn-sm btn-info'  p   = link_to 'Create new page', new_page_url, class: 'btn btn-sm btn-primary' 

И последняя вещь на сегодня — немного подправим _sidemenu.html.slim

#app/views/application/_sidemenu.html.slim   ul   li     = link_to 'Home', root_path, class: 'btn btn-sm btn-light' 

Думаю, на сегодня уже достаточно. Итак довольно объемная статья получилась. Проверьте все rubocop-ом, исправьте недочеты, если нужно, и можете смело заливать на ваш github. В следующей статье мы добавим пользователей, devise и настроим CI/CD. Надеюсь вам всем было интересно читать эту статью. Если есть какие-то замечания либо же предложения — смело пишите в комментариях. Желаю всем поменьше ошибок в коде и побольше интересных проектов!


ссылка на оригинал статьи https://habr.com/ru/post/652035/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *