We write a social network on Ruby on Rails. Part 1

Why and for whom is this article?
Hello everyone! I’m a Ruby on Rails Developer and I’ve only recently started my journey in this area. I have already gone through the first steps (I wrote about them in this article), like choosing a language, learning its basics, getting to know the framework, the first pet projects, the first interviews, the first offer, the first company. But many have just begun to follow this path, and this article is for them. I remember from experience how difficult it is to find guides (most of them are about creating bookstores, personal blogs, etc.), so I hope many people like the idea of creating a social network.
Why social network and how will the process go?
Firstly, I myself would be interested in the implementation of this project. Secondly, I was inspired by the book Practical Rails Social Networking Sites by Alan Bradburne. It would be possible to do everything according to the book, you say. But why then me and my articles? The book is from 2007 and the ruby version is 1.8, so most of the solutions won’t be relevant these days. In addition, I am not going to do everything according to the book, but only be guided by it (I will use the design from it with the addition bootsrap
). During development, I will use many gems that will be useful for novice developers. But I will say right away: this series of articles is not for the very start. Some basic stuff (setting ruby
, rails
what’s happened MVC
, git
and the like) I will skip, so I recommend postponing this series of articles and returning to it a little later. If you are an experienced developer and reading this article, I would really appreciate to hear your opinion. Regarding the frequency of publication of articles and exactly how many of them I can’t say, since I plan to do a project in my free time from work and write articles in parallel. But I will try not to procrastinate and do everything at a good pace.
Create a project and make initial settings
Before we start, install the following versions:
Ruby 3.0.3
Rails 6.1.4.6
MySQL 8.0
Node 10.19
Yarn 1.22.17
I strongly advise you to use github
during the development of this project. A little later we will set up CI/CD
on our project, which will be an extremely useful experience for you. Now let’s create our project. I will call him g_connect
but you can use any other name (if you choose another, wherever I will use g_connect
write your own).
rails new g_connect -d mysql
Now go to the folder with the project and take care of the primary settings. I always start with Gemfile
and some gems that I will definitely use during development.
#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]
Let me explain what some of these gems are for (I recommend reading the documentation before starting in more detail):
gem ‘aasm’ – we will use it for transactions between states
gem ‘bootstrap’ – we will need this for our design (I focus on the back and I will devote quite a bit of time to the front, but at the very end, inspiration for beauty will come)
gem ‘devise’ – authentication (short and clear, there will be another gem for authorization, but I have not chosen which one yet)
gem ‘slim’ – save time writing html tags
gem ‘better_errors’ – beautiful error output in the browser (for example, the route was specified incorrectly)
gem ‘factory_bot_rails’ – the template that we will use in our tests
gem ‘faker’ – to create fake data
gem ‘rspec-rails – we connect the testing environment
RSpec
for our projectgem ‘rubocop’ – check how well our code is written
gem ‘annotate’ – to automatically annotate our models (why switch between models and migrations if there is this gem)
gem ‘simplecov’ – to see if we covered everything with tests
Now we need to install all the gems and their dependencies, so we run bundle
in our terminal (by the way, yes, I forgot to say that I use Ubuntu
so for MacOS/Windows
(it’s better not to touch Windows at all when developing on ruby
, but if you really want to) see some points yourself). We can also delete the folder test
it is not needed (after all, we will write rspec
-and).
After that, we will set up our database. In file config/database.yml
specify your username and password (I did the standard root/root
). After that, we run the following (if someone does not know, then this one command immediately executes db:create
, db:schema:load
and db:seed
):
rails db:setup
Some of our gems require additional configuration. We will deal with them nowdevise
also requires additional configuration, but we will deal with this later when we do authentication). Let’s start with bootstrap
. Let’s go to the file app/assets/stylesheets/application.scss
(the file may initially have the extension .css
so fix it to .scss
) and add the following line:
/*app/assets/stylesheets/application.scss*/
@import "bootstrap";
Now let’s set up annotate
. To do this, run the following command in a terminal:
rails g annotate:install
Now you need to set up rspec
:
rails g rspec:install
Also for our tests we will need a setup factory_bot_rails
and simplecov
. in our folder spec
create a folder support
and in it we create a file factory_bot.rb
with the following code:
#spec/support/factory_bot.rb
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
end
Now let’s go to our spec/rails_helper.rb
. Here we will include our file for factory_bot
and also connect simplecov
(two lines to connect simplecov
should be at the very beginning of the file).
#spec/rails_helper.rb
require 'simplecov'
SimpleCov.start 'rails'
require_relative './support/factory_bot'
We’re done with setting up the gems for now. If you want to check the test coverage percentage, you can run xdg-open coverage/index.html
and that’s the magic. But I would like to add a couple more points. First – shared_context.rb
to test our models. In the folder spec/support
create 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
Our shared_context
will act as a template for model testing. In it, we describe what should happen if the data is valid and the object is created, and vice versa, what will happen if some data is invalid or missing and the object will not be created. This will greatly simplify writing tests for models, then in practice you will see this. Now let’s plug it into our spec_helper.rb
#spec/spec_helper.rb
require_relative './support/shared_context'
And the last thing about the settings before the start: a touch of design. Go to app/views/layouts/application.html.erb
. Change extension .erb
on the .slim
and do like this:
#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
I think it looks much better than .html.erb
. If you have never used .slim
then use here this resource for translation from .html.erb
v .html.slim
. Further in the folder app/views
create an application folder, and in it a file _sidemenu.html.slim
and inside it so far only the following lines:
#app/views/layouts/_sidemenu.html.slim
ul
li
= link_to 'Home', '/', class: 'btn btn-sm btn-light'
Then we go to app/assets/stylesheets/application.scss
and add the following:
/*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;
}
While quite simple (css
taken from the book), but, as I said earlier, I focus on the backing. When the initial work is over, we can move on. Can pour everything we’ve done to you on github
and do new things already in another branch. I recommend checking before doing this. rubocop
-om (you can add a special extension for your IDE
and it will immediately highlight files that have flaws for you).
Creating a page model
We will make the first model for pages. It will have the following fields
Field name | A type |
|
|
|
|
|
|
|
|
Permalink
will be made up of our title
only will have a more beautiful look to use as a url
. Let’s create a model.
rails g model Page
In addition to the model, we have generated a migration and two files for tests, we will return to them a little later. The migration is in the folder db/migrate
. Let’s start with her:
#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
After that we run rails db:migrate
(our annotate
immediately shows which files have been annotated) and go to our model. Here we will write our validations, as well as a method for obtaining our permalink
(we will call it using the callback after_create
). In this method I will use regular expressions, to help with composing them I always use 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
Now I propose to do our tests. Let’s start with a file spec/factories/pages.rb
#spec/factories/pages.rb
FactoryBot.define do
factory :page do
title { 'Test' }
body { 'Test' }
end
end
After that, we can start writing tests directly. It is in this file that our previously written 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
I think no explanation is needed here, just note that private methods do not need to be tested, so there are no tests for our clean_url
. We can launch rspec
in our terminal and make sure all our tests pass without errors. When we figured out the model and tests for them, I propose to deal with the controller. I don’t use a generator for controllers, so we create a file pages_controller.rb
in our folder app/controllers
. Here we will write the following:
#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
Here everything is more absolutely standard, so I will not focus on this either. Now we need to add routes for our controller:
#config/routes.rb
Rails.application.routes.draw do
root 'pages#index'
resources :pages
end
Now let’s cover our controller with tests. For it’s in a folder spec
create a folder controllers
and in the same place immediately create a file 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
Now I propose to make a visual for our controller. Go to app/views
and create a folder there pages
and it has 5 files: _form.htm.slim
, new.html.slim
, edit.html.slim
, show.html.slim
and index.html.slim
. Now let’s go through each of them. In our _form.htm.slim
there will be a form that we will fill out to create or change our Pages
. We will render this form in our new
and edit
respectively.
#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'
Now let’s get busy show
and 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'
And the last thing for today – a little tweak _sidemenu.html.slim
#app/views/application/_sidemenu.html.slim
ul
li
= link_to 'Home', root_path, class: 'btn btn-sm btn-light'
I think that’s enough for today. So this is a pretty big article. Check everything rubocop
-om, correct the shortcomings, if necessary, and you can safely upload to your github
. In the next article, we will add users, devise
and set up CI/CD
. I hope you all enjoyed reading this article. If you have any comments or suggestions – feel free to write in the comments. I wish everyone less errors in the code and more interesting projects!