Зачем и для кого эта статья?
Всем привет! Я 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
и он сразу будет подсвечивать вам файлы, в которых есть недочеты).
Создание модели страниц
Первую модель мы сделаем для страниц. У нее будут следующие поля
Имя поля |
Тип |
|
|
|
|
|
|
|
|
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
. Надеюсь вам всем было интересно читать эту статью. Если есть какие-то замечания либо же предложения - смело пишите в комментариях. Желаю всем поменьше ошибок в коде и побольше интересных проектов!
Sin2x
https://en.wikipedia.org/wiki/Diaspora_(social_network)