Здравствуйте, записался на курс. При этом ставил галочку на "обучаться с тьютором". На email пришло письмо, о том, что записался на самостоятельное изучение курса. Как выбрать тьютора? |
Микросообщения пользователей
Уничтожение микросообщений
Последний кусок функционала, добавляемый к ресурсу 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, в целях безопасности, хорошей практикой является выполнение поиска только через ассоциацию.
С кодом в этом разделе наша модель 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
Упражнения
Мы рассмотрели достаточно материала и теперь случился комбинаторный взрыв возможных расширений к нашему приложению. Вот лишь некоторые из многих возможностей:
- Добавить тесты для отображения количества микросообщений в сайдбаре (включая надлежащие плюрализации).
- Добавить тесты для пагинации микросообщений.
- Сделать рефакторинг Home страницы чтобы использовать отдельные партиалы для двух ветвей выражения if-else.
- Написать тест чтобы убедиться, что ссылки на удаление не появляются у микросообщений созданных не текущим пользователем.
- Удалить с помощью партиалов дублирование кода в удаляющих ссылках из Листинга 10.43 и Листинга 10.44.
- Сейчас очень длинные слова крушат наш шаблон, как это показано на рис. 10.18. Исправьте эту проблему с помощью хелпера wrap определенного в Листинге 10.47. Обратите внимание на использование метода raw для предотвращения маскирования Рельсами результирующего HTML, совместно с sanitize методом необходимым для предотвращения межсайтового скриптинга. Этот код также использует странно выглядящий, но полезный тернарный оператор (Блок 10.1).
- (сложное) Добавить 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.
________________________________________
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