Статьи

Просто сделай это: выучи Синатру, часть вторая

В первой части этого урока мы настроили Синатру и отобразили несколько страниц. Теперь самое интересное начинается — мы будем использовать базу данных для хранения наших задач.

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

Для взаимодействия с базой данных я буду использовать DataMapper . Это ORM, который работает аналогично Active Record, но имеет немного другую методологию и синтаксис.

Чтобы использовать DataMapper с SQLite, необходимо установить следующие гемы:

$> gem install data_mapper dm-sqlite-adapter sqlite3

Далее нам нужно убедиться, что гем DataMapper требуется в верхней части файла main.rb:

 require 'sinatra'
require 'data_mapper'

Теперь нам нужно установить соединение с базой данных, которое представляет собой всего одну строку кода, чтобы сообщить DataMapper, что мы будем использовать базу данных SQLite с именем «development.db», и она будет сохранена в той же папке, что и приложение:

 DataMapper.setup(:default, ENV['DATABASE_URL'] || "sqlite3://#{Dir.pwd}/development.db")

Модель задачи

Чтобы сохранить задачи в базе данных, нам нужно создать класс задач. Следующий код идет ниже соединения с базой данных в main.rb:

 class Task
  include DataMapper::Resource
  property :id,           Serial
  property :name,         String, :required => true
  property :completed_at, DateTime
end
DataMapper.finalize

Класс Task связан с DataMapper строкой include DataMapper::Resource После этой строки идут свойства класса Task. Свойство :idSerial После этого нам действительно нужны только два свойства для наших задач — название задачи (обратите внимание, что я сделал это обязательным свойством) и дата завершения. Метод DataMapper.finalize используется для проверки целостности ваших моделей. Его следует вызывать после ВСЕХ ваших моделей и до того, как ваше приложение начнет с ними взаимодействовать. На данный момент у нас есть только одна модель, поэтому она может идти только в конце определения класса.

Добавление задач

Для начала мы будем использовать IRB для создания нескольких задач. Откройте окно командной строки и введите следующее для доступа к приложению:

 $> irb
ruby-1.9.2-p180 :002 > require './main'
DataObjects::URI.new with arguments is deprecated, use a Hash of URI components (/home/daz/.rvm/gems/ruby-1.9.2-p180/gems/dm-do-adapter-1.1.0/lib/dm-do-adapter/adapter.rb:231:in `new')
=> true

Вы можете игнорировать предупреждение, которое появляется, когда вам требуется файл. Это связано с ошибкой в ​​DataMapper, которая должна быть исправлена ​​в следующем выпуске. Не волнуйтесь, это никак не повлияет на приложение.

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

 ruby-1.9.2-p180 :003 > Task.auto_migrate!
=> true

Теперь мы готовы добавить несколько задач. Это можно сделать в irb, используя метод create Вот несколько примеров добавления нескольких задач:

 ruby-1.9.2-p180 :004 > Task.create(name: "get the milk")
=> #
ruby-1.9.2-p180 :005 > Task.create(name: "order books") => #
ruby-1.9.2-p180 :006 > Task.create(name: "pick up dry cleaning")
=> #
ruby-1.9.2-p180 :007 > Task.create(name: "phone plumber")
=> #

Эти задачи теперь сохранены в базе данных. Если мы хотим их увидеть, нам просто нужно внести пару небольших изменений в обработчик в main.rb, который работает с корневым URL:

 get '/' do
  @tasks = Task.all
  slim :index
end

Task.all@tasks Вспомните, что переменные экземпляра доступны для всех представлений, поэтому я использую это в представлении index.slim, чтобы выполнить каждую задачу и отобразить их в виде списка:

 form action="/" method="POST"
  input type="text" name="task[name]"
  input.button type="submit" value="New Task >>"

h2 My Tasks

ul.tasks
  - @tasks.each do |task|
    li.task= task.name

Запустите сервер, набрав в командной строке $> ruby main.rb Это все хорошо, но мы хотим иметь возможность добавлять задачи с веб-страницы. Это не проблема — на самом деле, у нас уже есть форма для этого, нам просто нужно подключить ее к DataMapper, чтобы задачи сохранялись в базе данных.

В первой части урока был следующий обработчик, который использовался для обработки запроса POST при отправке формы:

 post '/' do
  @task =  params[:task]
  slim :task
end

Это просто нужно немного изменить на следующее:

 post '/' do
  Task.create  params[:task]
  redirect to('/')
end

Это создает новую задачу, используя информацию, хранящуюся в хэше params, который был представлен в форме (в данном случае это просто имя задачи). Затем он перенаправляет обратно на домашнюю страницу (на этот раз используя запрос GET), на которой должен отображаться список задач, включая новую задачу. Попробуйте в своем браузере.

Удаление задач

Пока все хорошо, мы можем добавлять задачи в наш список, но как насчет их удаления? Для этого нам нужно добавить кнопку удаления к каждой задаче, которая будет отправлять запрос DELETE на сервер. Пока я занимаюсь этим, я собираюсь разделить код представления для задач в отдельный файл. Поместите следующий код в файл, сохраненный как «task.slim» в папке представлений:

 li.task id=task.id
  = task.name
  form.delete action="/task/#{task.id}" method="POST"
    input type="hidden" name="_method" value="DELETE"
    input type="submit" value="×"  title="Delete Task"

Теперь измените index.slim на следующее:

 form action="/" method="POST"
  input type="text" name="task[name]"
  input.button type="submit" value="New Task >>"

h2 My Tasks

ul.tasks
  - @tasks.each do |task|
    == slim :task, locals: { task: task }

Здесь есть несколько вещей, на которые стоит обратить внимание. Во-первых, в представлении задач нам нужно создать форму для кнопки удаления. Это сделано для того, чтобы мы могли включить скрытое поле с атрибутами name="_method" value="DELETE" Это потому, что большинство браузеров не понимают HTTP-глаголы PUT и DELETE, только GET и POST. Чтобы обойти это, Sinatra использует скрытые поля формы, чтобы «обмануть» браузер и правильно обработать запрос. Это означает, что когда форма удаления отправлена, она будет обработана как запрос DELETE.

В индексном представлении обратите внимание, как я ссылался на представление задач, используя следующую строку:

 == slim :task, locals: { task: task }

Вот как Sinatra обрабатывает партиалы — вы просто называете шаблон встроенным. В этом случае я должен сказать ему, что ссылка, которую я делаю на задачу в файле task.slim, совпадает с задачей, которая передается в блок.

Осталось только написать обработчик для удаления задач. Вы заметите, что URL указан в атрибуте действия формы как /task/#{task.id} Соответствующий обработчик выглядит так:

 delete '/task/:id' do
  Task.get(params[:id]).destroy
  redirect to('/')
end

Этот код просто находит задачу, по которой щелкнули, используя идентификатор в URL, а затем использует метод destroy, чтобы удалить ее из базы данных. Затем он перенаправляет на корневую страницу, где будут перечислены остальные задачи.

Выполнение задач

Чтобы закончить, мы просто должны быть в состоянии выполнить задачи. Помните, что когда мы создавали класс Task, мы указали свойство completed_at Если для свойства установлено значение nil Откройте файл task.slim и измените его следующим образом:

 li.task
  = task.name
  form.update action="/task/#{task.id}" method="POST"
    input type="hidden" name="_method" value="PUT"
    -if task.completed_at.nil?
      input type="submit" value="  " title="Complete Task"
    -else
      input type="submit" value="✓" title="Uncomplete Task"
  form.delete action="/task/#{task.id}" method="POST"
    input type="hidden" name="_method" value="DELETE"
    input type="submit" value="×" title="Delete Task"

Теперь мы добавили строку, чтобы сообщить, когда задача была выполнена, если она была выполнена, и другую форму, которая содержит скрытое поле формы, указывающее, что это запрос PUT. Это потому, что мы модифицируем свойства задачи (на самом деле, должен использоваться запрос PATCH, но это не поддерживается до Sinatra 1.3). Мы также отображаем другую кнопку в зависимости от того, была ли задача выполнена или нет. Если задача была завершена, то на ней отображается галочка, если она не была выполнена, то это выглядит как пустой флажок, поэтому существует некоторая визуальная обратная связь для отображения статуса задачи.

Обратите внимание, что URL-адрес, указанный в атрибуте action, точно такой же — это потому, что Sinatra будет по-разному реагировать на любой используемый глагол HTTP. Теперь нам нужно написать обработчик для обработки этого запроса, поэтому добавьте его в конец main.rb:

 put '/task/:id' do
  task = Task.get params[:id]
  task.completed_at = task.completed_at.nil? ? Time.now : nil
  task.save
  redirect to('/')
end

Это находит задачу на основе идентификатора в URL, а затем устанавливает свойство complete_at на текущее время, если оно еще не было установлено, или возвращает его обратно на ноль, если оно было установлено. Другими словами, кнопка будет действовать как переключатель для переключения между выполненной и невыполненной задачей.

Перезагрузите сервер и попробуйте добавить, удалить и завершить задачи. Теперь у нас есть полностью функциональный список дел — всего менее 40 строк кода! Это немного грубо по краям и выглядит не лучшим образом, но мы разберемся с этим, а также добавим несколько списков в части 3!