Пишем REST приложение на Sinatra и прикручиваем Redactor. Часть 1

Данная статья рассчитана первым делом на новичков, которые только начинают изучать Ruby. После 3 недель изучения этого замечательного языка, накопились некоторые знания, которыми хочется поделиться.

Sinatra — бесплатный и открытый программный каркас написанный на языке Ruby, предназначенный для разработки веб-приложений. (ru.wikipedia.org)

Redactor — достаточно мощный и в тоже время простой и красивый в использовании wysiwyg редактор (ссылка на хабре)

Итак начнем!
Предположительно у вас уже установлен Ruby и SQLite.
Так как основным моментом статьи является попытка прикрутить Redactor, где для выбора уже загруженных изображений используются превью картинок, то для работы с изображениями установим Rmagick. В OS X установка совершается достаточно просто
curl https://raw.github.com/maddox/magick-installer/master/magick-installer.sh | sh

Теперь необходимо установить дополнительные gem'ы.
[sudo] gem install sinatra data_mapper carrierwave carrierwave/datamapper json shotgun

sintra — сам фреймворк
data_mapper — ORM
carrierwave — гем для загрузки изображений (использовался в одной статье на Хабре)
carrierwave/datamapper — для связи DataMapper и CarrierWave
json — для генерации данных в формате JSON

Нужные гемы установлены, теперь скачаем сам редактор redactorjs.com/ru/download

Приступим к написанию кода:
Классика жанра
# coding: utf-8
require 'sinatra'

get '/' do
  'REST приложение на Sinatra <a href="/posts">Перейти к страницам</a>'
end

сохраним к примеру под именем init.rb и запустим приложение shotgun init.rb
перейдем по адресу http://127.0.0.1:9393/


Вы заметили ссылку /posts, сейчас займемся ее работоспособностью.
Первое что мы должны сделать, это добавить установленные нами гемы в приложение
#init.rb
# coding: utf-8
require 'rubygems'
require 'sinatra'
require 'data_mapper'
require 'carrierwave'
require 'carrierwave/datamapper'
require 'rmagick'
require 'json'

Установим public папку с js скриптами и куда будем загружать изображения (также обычно в эту папку помещаются css файлы, изображения макетов страниц)
set :public_directory, './public'

И сразу создадим в ней папку js и скопируем туда ранее скаченный редактор

Далее напишем нашу модель Post
#init.rb
class Post
  include DataMapper::Resource
  property :id, Serial #Идентификатор
  property :title, String #String указывает на то, что поле title будет иметь тип "Строка" и стандартно не более 50 символов
  property :body, Text #Text указывает на то, что поле body будет иметь тип "Текст" размером 65535 символов
end

Создадим папку db в корне с нашим файлом init.rb
Настроим работу с базой данных
#init.rb
DataMapper.setup(:default, ENV['DATABASE_URL'] || 'sqlite:./db/base.db') #подключение и путь к бд
DataMapper.finalize #Проверка моделей
DataMapper.auto_upgrade!  #Создает новые таблицы и добавляет новые столбцы в существующие таблицы

Пропишем роутинг для /posts
#init.rb
#Список всех постов
get '/posts' do #роутинг для ссылки /posts
  @posts = Post.all #задаем переменную @posts равную выводу всех значений бд из модели Post
  erb :'index' #выводим все в шаблоне index.erb*
end

*по умолчанию Sinatra использует шаблонизатор erb — Erubis

Теперь создадим папку views в корне с нашим файлом init.rb, опять же по умолчанию Sinatra ищет шаблоны в папке views. Создадим в этой папке index.erb следующего содержания:
#index.erb
<strong><a href="/posts/new">Создать новую страницу</a></strong>
<h2>Список всех страниц</h2>
<% @posts.each do |post| %>
<strong><a href="/posts/<%= post.id %>/edit"><%= post.title %></a></strong><br>
<%= post.body %><br>
<% end %>

Разберем некоторе моменты:
Помните, мы с вами в запросе get '/posts' do задавали переменную @posts = Post.all? Так вот, конструкцией <% @posts.each do |post| %>.......<% end %> мы ищем в массиве все посты, имеющиеся в базе данных.
/posts/<%= post.id %>/edit — ссылка для редактирование поста, обращаться к постам мы будем через их id
<%= post.title %> — выводим в шаблоне Название поста
<%= post.body %> — выводим в шаблоне Тело поста

Сейчас нам необходимо создать общий шаблон для всех страниц, где будем подключать все пользовательские css и js. Создадим файл layout.erb в папке views и забегай немного вперед создадим в этой же директории папку posts и в ней 2 файла edit.erb и new.erb. Теперь структура приложения должна быть такой:

#layout.erb
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Redactor.js + Sinatra app</title>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

<%= yield %> — вместо этого будут выводится различные шаблоны которые мы создали — index.erb, new.erb, edit.erb

Теперь нам нужно, чтобы заработала ссылка /posts/new
Добавляем в init.rb
#init.rb
#Create new Post
get '/posts/new' do #роутинг для ссылки создания страницы
  erb :'posts/new' #рендерим шаблон /posts/new.erb
end

post '/posts/new' do
  params.delete 'submit'
  @post = Post.create(params) #сохраняем данные, полученные из формы в базу данных модели Post
  redirect '/posts' #редиректимся на страницу со всеми постами
end

И дописываем в views/posts/new.erb
#new.erb
<h3>Создать</h3>
<form method="post" action="/posts/new">
  <label for="title"><strong>Название:</strong></label><br>
  <input id="title" type="text" name="title" value="" style="width: 250px;">
  <br>
  <label for="body"><strong>Тело страницы:</strong></label><br>
  <textarea id="body" name="body" style="height: 250px;"></textarea>
  <br>
  <input type="submit" name="submit" value="Создать">
</form>

Переходим по ссылке «Создать новую страницу»

Добавляем данные, жмем кнопку «Создать», пост создан.

Теперь добавим немного кода для редактирования и удаления страниц.
#init.rb
#Edit post
get '/posts/:id/edit' do #роутинг для редактирования поста, доступ к посту получаем по его id
  @post = Post.get(params[:id]) #в переменной @post получаем данные из бд согласно id поста
  erb :'posts/edit' #рендерим шаблон /posts/edit.erb
end

#Update post
put '/posts/:id/edit' do
  post = Post.get(params[:id])
  post.title = (params[:title]) 
  post.body = (params[:body])
  post.save #сохраняем измененные данные
  redirect '/posts' #редиректимся на страницу со всеми постами
end

#Delete post
get '/posts/:id/delete' do
  Post.get(params[:id]).destroy #удаляем пост
  redirect '/posts'
end

Отредактируем views/posts/edit.erb
#edit.erb
<h3>Редактировать</h3>
<a href="/posts/<%= @post.id %>/delete">Удалить страницу</a><br><br>
<form action="/posts/<%= @post.id %>/edit" method="post">
  <input name="_method" type="hidden" value="put" />
  <label for="title"><strong>Название:</strong></label><br>
  <input type="text" name="title" id="title" value="<%= @post.title %>">
  <br>
  <label for="body"><strong>Тело страницы:</strong></label><br>
  <textarea id="body" name="body" style="height: 250px;"><%= @post.body %></textarea>
  <br>
  <input id="post_submit" name="commit" type="submit" value="Сохранить">
</form>

По сути приложение готово к использованию, теперь о второй части статьи, это прикрутим Redactor к textarea

Сначала подключим его. Открываем views/layout.erb, добавляем после
#layout.erb
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script type="text/javascript" src="/js/redactor/redactor.js"></script>
<link rel="stylesheet" href="/js/redactor/css/redactor.css" type="text/css">

Теперь настроем (более подробно о настройке можно прочитать тут).
#layout.erb
<script type="text/javascript">
      $(document).ready(function() {
        $('.redactor_1').redactor({toolbar: 'default', lang: 'ru', imageUpload: '/upload/image', imageGetJson: '/uploads/images/imageslist.json'});
      });
</script>

$('.redactor_1') — вызываем редактор с именем redactor_1
toolbar: 'default' — устанавливаем стандартный тулбар со всеми кнопками
lang: 'ru' — устанавливаем русский язык
imageUpload: '/upload/image' — так мы будем загружать изображения
imageGetJson: '/uploads/images/imageslist.json' — так мы будем получать превью и список уже загруженных изображений

Теперь вызовем редактор для textarea, для этого откроем views/posts/new.erb и views/posts/edit.erb и добавим класс class=«redactor_1». Для примера new.erb должен выглядеть сейчас так:
#new.erb
<h3>Создать</h3>
<form method="post" action="/posts/new">
  <label for="title"><strong>Название:</strong></label><br>
  <input id="title" type="text" name="title" value="" style="width: 250px;">
  <br>
  <label for="body"><strong>Тело страницы:</strong></label><br>
  <textarea id="body" class="redactor_1" name="body" style="height: 250px;"></textarea>
  <br>
  <input type="submit" name="submit" value="Создать">
</form>

И edit.erb так:
#edit.erb
<h3>Редактировать</h3>
<a href="/posts/<%= @post.id %>/delete">Удалить страницу</a><br><br>
<form action="/posts/<%= @post.id %>/edit" method="post">
  <input name="_method" type="hidden" value="put" />
  <label for="title"><strong>Название:</strong></label><br>
  <input type="text" name="title" id="title" value="<%= @post.title %>">
  <br>
  <label for="body"><strong>Тело страницы:</strong></label><br>
  <textarea id="body" class="redactor_1" name="body" style="height: 250px;"><%= @post.body %></textarea>
  <br>
  <input id="post_submit" name="commit" type="submit" value="Сохранить">
</form>

Создадим в папке public папку uploads в ней папку images. Структура директорий получится public/uploads/images.
Теперь реализуем загрузку изображений через gem CarrierWave. Настраиваем в init.rb:
#init.rb
class ImageUploader < CarrierWave::Uploader::Base
  def store_dir
    'uploads/images' #устанавливаем папку для загружаемых изображений
  end
  def extension_white_list
    %w(jpg jpeg gif png bmp) #разрешаем загружать  только jpg, jpeg, gif, png, bmp
  end
  include CarrierWave::RMagick #подключаем RMagick для создания превью
  version :thumb do
    process :resize_to_fill => [100,74] #говорим RMagick'у какой нам нужен размер для превью
  end
  storage :file
end

Так как Redactor показывает нам уже загруженные изображения через их превью, которые в свою очередь читает из JSON файла, который у нас назвается imageslist.json и лежит в public/uploads/images/. Нам нужно привести этот самый файл к формату, который понимает Redactor.
{ "thumb": "/tests/_images/1_m.jpg", "image": "/tests/_images/1.jpg" },

Отсюда и вытекает, что нам нужно создать модель следующим образом
#init.rb
class UploadedImages #создаем модель в бд загруженных изображений
  include DataMapper::Resource
  property :id,    Serial
  property :image, String # 
  property :thumb, String # 

  mount_uploader :file, ImageUploader #прикручиваем загрузчик carrierwave
end

В самый конец файла init.rb добавляем следущее:
#init.rb
post '/upload/image' do
  params[:file]
  filename = params[:file][:filename]
  file = params[:file][:tempfile]
  upload = UploadedImages.new
  upload.file = params[:file]
  upload.image = params[:image] = '/uploads/images/' + File.join(filename) #записали в бд путь до изображения
  upload.thumb = params[:thumb] = '/uploads/images/thumb_' + File.join(filename) #записали в бд путь до превью изображения
  upload.save #загрузили - сохранили
  @images = UploadedImages.all #вводим новую переменную @images для вывода всех загруженных изображений и их превью  в JSON
  File.open("public/uploads/images/imageslist.json","w") do |f| #открываем imageslist.json для записи
    f.write JSON.pretty_generate(@images) #генерируем JSON формат в файл всех загруженных изображений
  end
  '<img src="/uploads/images/' + File.join(filename) + '" />'# вставляем ссылку в textarea со свежезагруженным изображением
end


Что происходит? При загрузке изображения удаляется содержимое imageslist.json и генерируется новое. Честно говоря, не знаю насколько это накладно для сервера.

Если статья найдет свой отклик, в следующий раз я постараюсь описать процесс загрузки файлов через этот редактор, а так же интерфейс управления загруженными изображениями.

Исходный код можно посмотреть на github.

P.S.: Прошу не сильно ругать меня, старался изложить все по полкам. Конечно понимаю что мой код далек от идеала, и некоторые моменты можно реализовать более просто, но думаю это неплохая отправная точка для начала изучения Ruby и Sinatra. Спасибо.

P.S.S.: Везде где попадается
<hh user=posts>
или
<hh user=images>
просто замените на @posts и на images соответственно.
  • +15
  • 6.4k
  • 8
Share post

Similar posts

Comments 8

    +1
    gem install slim
    


    slim-lang.com/
      +1
      gem install haml
        0
        Как вариант :) Хотя мне Slim нравится больше. Он чище.
        0
        вы что, смеётесь все?

        echo «gem 'haml/slim'» >> Gemfile
        bundle install
          0
          :)

          или даже просто
          echo "gem slim" >> Gemfile
          bundle
          
        0
        get '/posts/:id/delete' do
        Никогда не делайте так в жизни! Иначе первый же залетевший гугл-бот разрушит вашу цивилизацию.
          0
          Да это же понятно. Подобные запросы должны быть только через административную панель с условием что пользователь администратор/редактор.
            0
            Для полноценного REST маршруты должны быть такими:

            # Список всех публикаций
            get '/posts'
            
            # Одна публикация
            get '/posts/:id'
            
            # Форма для добавления публикации
            get '/posts/new'
            
            # Сохранение публикации
            post '/posts'
            
            # Форма для редактировани публикации
            get '/posts/:id/edit'
            
            # Обновление отредактированной публикации
            put '/posts/:id'
            
            # Стриница с предупреждением об удалении публикации
            get '/posts/:id/delete'
            
            # Удаление публикации
            delete '/posts/:id'
            

            Only users with full accounts can post comments. Log in, please.