Опубликован: 27.01.2016 | Уровень: для всех | Доступ: платный
Лекция 10:

Микросообщения пользователей

< Лекция 9 || Лекция 10: 123456 || Лекция 11 >

Уничтожение микросообщений

Последний кусок функционала, добавляемый к ресурсу Microposts это воможность уничтожения микросообщений. Как и с удалением пользователя (Раздел 9.4.2), мы будем делать это с помощью "delete" ссылок, как показано на рис. 10.16. В отличие от уничтожения пользователя, где право на удаление имели только администраторы, удаляющие ссылки будут работать только для пользователя, создавшего микросообщения.

Набросок предварительной реализации потока сообщений со ссылками на удаление микросообщений.

Рис. 10.16. Набросок предварительной реализации потока сообщений со ссылками на удаление микросообщений.

Нашим первым шагом является добавление удаляющей ссылки в партиал микросообщения как в Листинге 10.40, и пока мы в нем, мы добавим похожую ссылку к партиалу элемента потока из Листинга 10.40. Результат представлен в Листинге 10.43 и Листинге 10.44. (Эти два случая почти идентичны и устранение этого дублирования остается в качестве упражнения (Раздел 10.5).)

<li>
  <span class="content"><%= micropost.content %></span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
  </span>
  <% if current_user?(micropost.user) %>
    <%= link_to "delete", micropost, method: :delete,
                                     data: { confirm: "You sure?" },
                                     title: micropost.content %>
  <% end %>
</li>
Листинг 10.43.
<li id="<%= feed_item.id %>">
  <%= link_to gravatar_for(feed_item.user), feed_item.user %>
    <span class="user">
      <%= link_to feed_item.user.name, feed_item.user %>
    </span>
    <span class="content"><%= feed_item.content %></span>
    <span class="timestamp">
      Posted <%= time_ago_in_words(feed_item.created_at) %> ago.
    </span>
  <% if current_user?(feed_item.user) %>
    <%= link_to "delete", feed_item, method: :delete,
                                     data: { confirm: "You sure?" },
                                     title: feed_item.content %>
  <% end %>
</li>
Листинг 10.44. Партиал элемента потока микросообщений с добавленной ссылкой на удаление. app/views/shared/_feed_item.html.erb

Тест на удаление микросообщений использует Capybara для клика по ссылке "delete" и ожидает что количество Микросообщений уменьшится на 1 (Листинг 10.45).

require 'spec_helper'

describe "Micropost pages" do
  .
  .
  .
  describe "micropost destruction" do
    before { FactoryGirl.create(:micropost, user: user) }

    describe "as correct user" do
      before { visit root_path }

      it "should delete a micropost" do
        expect { click_link "delete" }.to change(Micropost, :count).by(-1)
      end
    end
  end
end
Листинг 10.45. Тест для действия destroy контроллера Microposts. spec/requests/micropost_pages_spec.rb

Код приложения также аналогичен коду для удаления пользователей из Листинга 9.46; главное отличие в том, что вместо использования предфильтра admin_user, в случае микросообщений мы используем предфильтр correct_user для проверки того, что текущий пользователь действительно имеет микросообщение с данным id. Код представлен в Листинге 10.46, а результаты уничтожения предпоследнего сообщения представлены на рис. 10.17.

class MicropostsController < ApplicationController
  before_action :signed_in_user, only: [:create, :destroy]
  before_action :correct_user,   only: :destroy
  .
  .
  .
  def destroy
    @micropost.destroy
    redirect_to root_url
  end

  private

    def micropost_params
      params.require(:micropost).permit(:content)
    end

    def correct_user
      @micropost = current_user.microposts.find_by(id: params[:id])
      redirect_to root_url if @micropost.nil?
    end
end
Листинг 10.46. Действие destroy контроллера Microposts. app/controllers/microposts_controller.rb

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

current_user.microposts.find_by(id: params[:id])

Это автоматически обеспечивает поиск лишь микросообщений принадлежащих текущему пользователю. В данном случае мы используем find_by вместо find так как последнее вызывает исключение в случае если микросообщение не существует вместо того, чтобы вернуть nil. Кстати, если вы хорошо знакомы с исключениями в Ruby, вы также можете написать фильтр correct_user вроде этого:

def correct_user
  @micropost = current_user.microposts.find(params[:id])
rescue
  redirect_to root_url
end

Мы могли бы реализовать фильтр correct_user непосредственно через модель Micropost, например так:

@micropost = Micropost.find_by(id: params[:id])
redirect_to root_url unless current_user?(@micropost.user)

Это было бы эквивалентно коду в Листинге 10.46, но, как объяснял Wolfram Arnold с своем блоге Access Control 101 in Rails and the Citibank Hack, в целях безопасности, хорошей практикой является выполнение поиска только через ассоциацию.

Home страница пользователя после удаления предпоследнего микросообщения.

Рис. 10.17. Home страница пользователя после удаления предпоследнего микросообщения.

С кодом в этом разделе наша модель Micropost и интерфейс завершены и набор тестов должен пройти:

$ bundle exec rspec spec/

Заключение

Добавив ресурс Microposts, мы почти закончили наш пример приложения. Все, что осталось, это добавить социальный слой, позволив пользователям следовать друг за другом. Мы узнаем как моделировать такие отношения пользователей и увидим полноценную реализацию потока сообщений в Главе 11.

Прежде чем продолжать, убедитесь что вы закоммитили и объединили ваши изменения если вы используете Git для контроля версий:

$ git add .
$ git commit -m "Add user microposts"
$ git checkout master
$ git merge user-microposts
$ git push

Вы также можете отправить приложение на Heroku. Поскольку модель данных изменилась из-за добавления таблицы microposts, вам также потребуется запустить миграцию продакшен базы данных:

$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rake db:migrate
$ heroku run rake db:populate

Упражнения

Мы рассмотрели достаточно материала и теперь случился комбинаторный взрыв возможных расширений к нашему приложению. Вот лишь некоторые из многих возможностей:

  1. Добавить тесты для отображения количества микросообщений в сайдбаре (включая надлежащие плюрализации).
  2. Добавить тесты для пагинации микросообщений.
  3. Сделать рефакторинг Home страницы чтобы использовать отдельные партиалы для двух ветвей выражения if-else.
  4. Написать тест чтобы убедиться, что ссылки на удаление не появляются у микросообщений созданных не текущим пользователем.
  5. Удалить с помощью партиалов дублирование кода в удаляющих ссылках из Листинга 10.43 и Листинга 10.44.
  6. Сейчас очень длинные слова крушат наш шаблон, как это показано на рис. 10.18. Исправьте эту проблему с помощью хелпера wrap определенного в Листинге 10.47. Обратите внимание на использование метода raw для предотвращения маскирования Рельсами результирующего HTML, совместно с sanitize методом необходимым для предотвращения межсайтового скриптинга. Этот код также использует странно выглядящий, но полезный тернарный оператор (Блок 10.1).
  7. (сложное) Добавить JavaScript отображение к Home странице для обратного отсчета 140 знаков.

________________________________________

Блок 10.1.10 типов людей

В мире существует 10 типов людей: Те, кому нравится тернарный оператор, те, кому он не нравится, и те, кто не знает о нем. (Если вам случилось быть в третьей категории, скоро вы ее покинете.)

Когда вы много программируете, вы быстро узнаете, что одним из наиболее распространенных битов управления потоком, является что то вроде этого:

 if boolean?
    do_one_thing
  else
    do_something_else
  end

Ruby, как и многие другие языки (включая C/C++, Perl, PHP, и Java), позволяет заменить это на гораздо более компактное выражение с помощью тернарного оператора (названного таким образом, потому что он состоит из трех частей):

boolean? ? do_one_thing : do_something_else

Вы можете также использовать тернарный оператор в качестве замены для назначения:

if boolean?
    var = foo
  else
    var = bar
  end

становится

var = boolean? ? foo : bar

Другим распространенным случаем использования является возвращаемое значение функции:

def foo
    do_stuff
    boolean? ? "bar" : "baz"
  end

оскольку Ruby неявно возвращает значение последнего выражения в функции, здесь метод foo возвращает "bar" или "baz" в зависимости от значения boolean?. Именно эта конструкция представлена в Листинге 10.47.

________________________________________

(Порушенный) особенно длинным словом шаблон сайта.

Рис. 10.18. (Порушенный) особенно длинным словом шаблон сайта.
module MicropostsHelper

  def wrap(content)
    sanitize(raw(content.split.map{ |s| wrap_long_string(s) }.join(' ')))
  end

  private

    def wrap_long_string(text, max_width = 30)
      zero_width_space = "​"
      regex = /.{1,#{max_width}}/
      (text.length < max_width) ? text :
                                  text.scan(regex).join(zero_width_space)
    end
end
Листинг 10.47. Хелпер для упаковки длинных слов. app/helpers/microposts_helper.rb
< Лекция 9 || Лекция 10: 123456 || Лекция 11 >
Вадим Обозин
Вадим Обозин

Здравствуйте, записался на курс. При этом ставил галочку на "обучаться с тьютором". На email пришло письмо, о том, что записался на самостоятельное изучение курса. Как выбрать тьютора?

Акбар Ахвердов
Акбар Ахвердов
Россия, г. Москва
Артём Зайцев
Артём Зайцев
Украина, ДНР