Вот несколько вопросов для вас:
- Вы когда-нибудь участвовали в опросах на сайте? Да нет?
 - Вы сами создали опросы? Да нет?
 - Как насчет создания сегодня веб-приложения, позволяющего создавать пользователям собственные опросы и участвовать в них ?! Да нет?
 
В этой статье я собираюсь показать вам, как создать веб-приложение, которое позволяет аутентифицированным пользователям создавать, управлять и участвовать в опросах. При его создании мы обсудим следующее:
- Вложенные атрибуты в Rails и самоцвете Cocoon
 - Отношения многие ко многим с промежуточной таблицей
 - Аутентификация через Facebook с использованием стратегии OmniAuth
 - Использование плагина jQuery Validate и AJAX для улучшения работы пользователя
 - Кеширование моделей, счетчики кэшей (с гемом cache_culture) и энергичная загрузка для повышения производительности
 - Визуализация статистики опроса с помощью индикаторов прогресса Bootstrap (и немного математики)
 
Все это займет всего шесть итераций! Здорово? Тогда начнем!
Исходный код доступен на GitHub .
Рабочую демонстрацию можно найти по адресу http://sitepoint-poller.herokuapp.com .
Некоторые наземные работы
Я не смог придумать какое-нибудь классное название для нашего веб-сервиса, поэтому для простоты мы назовем его Poller. Создайте новое приложение Rails без набора тестов по умолчанию:
$ rails new poller -T
У нас будет много опросов, созданных разными пользователями с неограниченным количеством вариантов голосования. Создание единой таблицы, в которой будут содержаться как темы для голосования, так и список возможных вариантов, нецелесообразно, поскольку каждый пользователь может голосовать только один раз. Кроме того, опция, которую выбирает каждый пользователь, также будет записана для подсчета общего количества голосов. Таким образом, связь между опцией голосования и пользователем будет существовать.
  Поэтому давайте создадим две отдельные таблицы — pollsvote_options  Первая таблица имеет только одно поле (кроме idcreated_atupdated_at 
-   
topictext 
  Таблица vote_options 
-   
titlestring -   
poll_idintegervote_optionpolls 
Создайте и примените соответствующие миграции:
 $ rails g model Poll topic:text
$ rails g model VoteOption title:string poll:references
$ rake db:migrate
Измените файлы модели, добавив связи и некоторые проверки:
модели / poll.rb
 [...]
has_many :vote_options, dependent: :destroy
validates :topic, presence: true
[...]
модели / vote_option.rb
 [...]
validates :title, presence: true
[...]
  Теперь пришло время разобраться с представлениями, контроллерами и маршрутами.  Создайте PollsController 
polls_controller.rb
 class PollsController < ApplicationController
  def index
    @polls = Poll.all
  end
  def new
    @poll = Poll.new
  end
  def create
    @poll = Poll.new(poll_params)
    if @poll.save
      flash[:success] = 'Poll was created!'
      redirect_to polls_path
    else
      render 'new'
    end
  end
  private
  def poll_params
    params.require(:poll).permit(:topic)
  end
end
и соответствующие маршруты:
конфиг / routes.rb
 resources :polls
root to: 'polls#index'
Я фанат Twitter Bootstrap, чтобы помочь нам создать красивый дизайн, поэтому добавьте его в Gemfile :
Gemfile
 [...]
gem 'bootstrap-sass'
[...]
  Не забудьте запустить bundle install 
Переименуйте ваш application.css в application.css.scss и замените его содержимое на:
 @import 'bootstrap';
@import 'bootstrap/theme';
Измените макет следующим образом:
макеты / application.html.erb
 [...]
<div class="navbar navbar-inverse">
  <div class="container">
    <div class="navbar-header">
      <%= link_to 'Poller', root_path, class: 'navbar-brand' %>
    </div>
    <ul class="nav navbar-nav">
      <li><%= link_to 'Add poll', new_poll_path %></li>
    </ul>
  </div>
</div>
<div class="container">
  <% flash.each do |key, value| %>
    <div class="alert alert-<%= key %>">
      <%= value %>
    </div>
  <% end %>
  <div class="page-header">
    <h1><%= yield :page_header %></h1>
  </div>
  <%= yield %>
</div>
[...]
  Мы используем yield :page_headerdivh1 
Создайте несколько видов:
опросы / index.html.erb
 <% content_for(:page_header) {"Participate in our polls right now!"} %>
опросы / new.html.erb
 <% content_for(:page_header) {"Create a new poll"} %>
<%= render 'form' %>
опросы / _form.html.erb
 <%= form_for @poll do |f| %>
  <%= render 'shared/errors', object: @poll %>
  <div class="form-group">
    <%= f.label :topic %>
    <%= f.text_area :topic, rows: 3, required: true, class: 'form-control' %>
  </div>
  <%= f.submit 'Create', class: 'btn btn-primary btn-lg' %>
<% end %>
общий / _errors.html.erb
 <% if object.errors.any? %>
  <div class="alert alert-warning">
    <h4>The following errors were found:</h4>
    <ul>
      <% object.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
    </ul>
  </div>
<% end %>
Земляная работа завершена, и мы готовы перейти к сути статьи.
Создание опций голосования
Остановись на секунду и взгляни на то, что уже сделано. У нас есть несколько методов контроллера и несколько представлений для просмотра и создания новых опросов. Однако у нас нет страницы для создания вариантов голосования для определенного опроса. Должен ли отдельный контроллер и представление быть созданы для этой цели? Предположим, вам нужно десять вариантов голосования, хотите ли вы отправить форму десять раз?
Создание вариантов голосования наряду с созданием опроса намного лучше. Это может быть достигнуто путем использования вложенных атрибутов Rails, чтобы позволить сохранять атрибуты связанных записей через родительский элемент.
  Прежде всего, мы должны включить вложенные атрибуты в poll.rb (потому что PollVoteOption 
модели / poll.rb
 [...]
accepts_nested_attributes_for :vote_options, :reject_if => :all_blank, :allow_destroy => true
[...]
  :reject_if => :all_blank 
  :allow_destroy => true 
  редактировать страницу опроса, которую мы вскоре создадим). 
Чтобы разрешить создание неограниченного количества вариантов голосования на одной странице, вы можете написать несколько вспомогательных функций и немного JavaScript. Однако для этой демонстрации мы будем использовать драгоценный камень Cocoon, созданный Натаном Ван дер Аувера, чтобы помочь нам быстро достичь желаемого результата. Этот гем помогает в создании динамических вложенных форм и работает с базовыми формами Rails, Formtastic и Simple_form.
Добавьте эти драгоценные камни в свой Gemfile:
Gemfile
 [...]
gem 'jquery-turbolinks'
gem "cocoon"
[...]
  и запустите bundle install 
  jquery-turbolinks следует добавлять, только если вы используете Turbolinks .  Он возвращает событие jQuery document.ready 
Включите соответствующие файлы JavaScript:
application.js
 [...]
//= require jquery.turbolinks
//= require cocoon
[...]
Последнее, что нужно сделать перед началом построения вложенной формы, это немного изменить метод контроллера, чтобы разрешить некоторые новые атрибуты:
polls_controller.rb
 [...]
def poll_params
  params.require(:poll).permit(:topic, vote_options_attributes: [:id, :title, :_destroy])
end
[...]
  Разрешение :_destroy 
Давайте перейдем к актуальной форме:
опросы / _form.html.erb
 <%= form_for @poll do |f| %>
  <%= render 'shared/errors', object: @poll %>
  <div class="form-group">
    <%= f.label :topic %>
    <%= f.text_area :topic, rows: 3, required: true, class: 'form-control' %>
  </div>
  <div class="panel panel-default">
    <div class="panel-heading">Options</div>
    <div class="panel-body">
      <%= f.fields_for :vote_options do |options_form| %>
        <%= render 'vote_option_fields', f: options_form %>
      <% end %>
      <div class="links">
        <%= link_to_add_association 'add option', f, :vote_options %>
      </div>
    </div>
  </div>
  <%= f.submit 'Create', class: 'btn btn-primary btn-lg' %>
<% end %>
  Вспомогательный метод fields_forvote_option_fieldslink_to_add_association 
  div class="links"  Этот метод принимает имя ссылки для отображения на странице, объект построителя форм и множественное имя ассоциации.  Необходимо обернуть этого помощника partial  Кроме того, этот помощник ожидает найти частичные « singular_association_name _fields» внутри того же каталога, откуда он был вызван.  Если вы хотите, чтобы он использовал другое частичное, используйте link_to_add_association 'add something', f, :somethings, 
    :partial => 'shared/something_fields'
 <div class="nested-fields">
  <div class="form-group">
    <%= f.label :title %>
    <%= f.text_field :title, class: 'form-control', required: true %>
  </div>
  <%= link_to_remove_association "remove option", f %>
</div>
Есть много других опций, которые можно передать этому помощнику.
Создайте новый фрагмент:
опросы / vote_option_fields.html.erb
 nested-fields
  Здесь требуется обертка с классом link_to_remove_association  :allow_destroy => true  Когда отправляется родительская форма, соответствующая запись также удаляется (при условии, что вы указали accepts_nested_attributes_for_destroyindex 
Загрузите свой сервер и попробуйте создать опрос и некоторые связанные параметры. Кажется довольно легко, а?
Перечисление и управление опросами
  Мы должны связать некоторые свободные концы в этой точке.  В частности, страница [...]
def edit
  @poll = Poll.find_by_id(params[:id])
end
def update
  @poll = Poll.find_by_id(params[:id])
  if @poll.update_attributes(poll_params)
    flash[:success] = 'Poll was updated!'
    redirect_to polls_path
  else
    render 'edit'
  end
end
def destroy
  @poll = Poll.find_by_id(params[:id])
  if @poll.destroy
    flash[:success] = 'Poll was destroyed!'
  else
    flash[:warning] = 'Error destroying poll...'
  end
  redirect_to polls_path
end
[...]  Это можно легко исправить. 
Сначала добавьте еще несколько методов в контроллер:
polls_controller.rb
 [...]
<% @polls.each do |poll| %>
  <div class="well">
    <h2><%= poll.topic %></h2>
    <div class="btn-group">
      <%= link_to 'Edit', edit_poll_path(poll), class: 'btn btn-default' %>
      <%= link_to 'Delete', poll_path(poll),
                  method: :delete,
                  class: 'btn btn-danger', data: {confirm: 'Are you sure?'} %>
    </div>
  </div>
<% end %>
Затем добавьте код для отображения всех доступных опросов:
опросы / index.html.erb
 .well {
  h2 {
    margin-top: 0;
  }
}
Также добавьте простой стиль для удаления верхнего поля для заголовков:
application.css.scss
 <% content_for(:page_header) {"Edit poll"} %>
<%= render 'form' %>
Наконец, создайте представление редактирования:
опросы / edit.html.erb
 [...]
gem 'omniauth-facebook'
[...]
На данный момент, опросы сделаны. Пришло время реализовать основные функции: аутентификацию и фактическое голосование.
Аутентификация
Давайте позволим пользователям проходить аутентификацию через Facebook. Для этого нам потребуется стратегия Facebook для OmniAuth , которую я описал в одном из моих предыдущих постов .
Gemfile
 bundle install
  Затем запустите Rails.application.config.middleware.use OmniAuth::Builder do 
  provider  :facebook,
            ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET'],
            scope: 'public_profile', display: 'page', image_size: 'square'
end
Хорошо, теперь настройте вашего нового провайдера Facebook, создав файл omniauth.rb в каталоге config / initializers со следующим содержимым:
конфиг / Инициализаторы / omniauth.rb
 uid
Получите пару ключ-секрет, создав новое приложение на странице разработчиков Facebook, откройте вкладку «Настройки» и добавьте следующую информацию:
- Добавить новую «Платформу» («Веб-сайт»)
 - Заполните «URL сайта» с адресом вашего сайта
 - Заполните «Домены приложения» (должен быть получен из URL сайта)
 - Заполните контактный E-mail.
 
Затем перейдите на вкладку «Статус и обзор» и сделайте ваше приложение активным (это делает его доступным для всех, к вашему сведению). Вернитесь в «Dashboard» и обратите внимание на «App ID» и «App Secret» — вот те ключи, которые вы ищете. Добавьте к инициализатору OmniAuth.
Создать таблицу для хранения информации о пользователе. Нам понадобятся только некоторые основные данные:
-   
stringname -   
stringimage_url -   
string$ rails g model User name:string image_url:string uid:string:index 
Создайте соответствующую миграцию:
 uid
  Этот $ rake db:migrate 
Теперь примените вашу миграцию:
 get '/auth/:provider/callback', to: 'sessions#create'
get '/auth/failure', to: 'sessions#auth_fail'
get '/sign_out', to: 'sessions#destroy', as: :sign_out
Добавьте несколько новых маршрутов:
routes.rb
 /auth/:provider/callback
  В маршруте class SessionsController < ApplicationController
  def create
    user = User.from_omniauth(request.env['omniauth.auth'])
    cookies[:user_id] = user.id
    flash[:success] = "Welcome, #{user.name}!"
    redirect_to root_url
  end
  def destroy
    cookies.delete(:user_id)
    flash[:success] = "Goodbye!"
    redirect_to root_url
  end
  def auth_fail
    render text: "You've tried to authenticate via #{params[:strategy]}, but the following error
occurred: #{params[:message]}", status: 500
  end
end 
На контроллере:
sessions_controller.rb
 request.env['omniauth.auth']
  from_omniauth  Метод class User < ActiveRecord::Base  Как видите, в этом контроллере ничего особенного не происходит. 
  class << self
    def from_omniauth(auth)
      uid = auth.uid
      info = auth.info.symbolize_keys!
      user = User.find_or_initialize_by(uid: uid)
      user.name = info.name
      user.image_url = info.image
      user.save!
      user
    end
  end
end
модели / user.rb
 current_user
  Нам понадобится метод для проверки подлинности пользователя.  Традиционно это называется [...]
def current_user
  @current_user ||= User.find_by(id: cookies[:user_id]) if cookies[:user_id]
end
helper_method :current_user
[...] 
application_controller.rb
 helper_method
  current_user[...] 
<ul class="nav navbar-nav">
  <li><%= link_to 'Add poll', new_poll_path %></li>
</ul>
<ul class="nav navbar-nav navbar-right">
  <% if current_user %>
    <li><%= image_tag current_user.image_url, alt: current_user.name %></li>
    <li><%= link_to 'Logout', sign_out_path %></li>
  <% else %>
    <li><%= link_to 'Sign in', '/auth/facebook' %></li>
  <% end %>
</ul>
[...]
Отлично. Последнее, что нужно сделать, это разрешить пользователю аутентифицировать или отображать информацию о пользователе вместе со ссылкой «Выйти», если вы уже вошли в систему:
макеты / application.html.erb
 <% @polls.each do |poll| %>
  <div class="well">
    <h2><%= poll.topic %></h2>
    <p>
      <% if current_user %>
        <%= link_to 'Participate!', poll_path(poll), class: 'btn btn-primary btn-lg block' %>
      <% else %>
        Please sign in via <%= link_to 'Facebook', '/auth/facebook' %> to participate in this poll.
      <% end %>
    </p>
    <div class="btn-group">
      <%= link_to 'Edit', edit_poll_path(poll), class: 'btn btn-default' %>
      <%= link_to 'Delete', poll_path(poll),
                  method: :delete,
                  class: 'btn btn-danger', data: {confirm: 'Are you sure?'} %>
    </div>
  </div>
<% end %>
Прежде чем перейти к следующей итерации, давайте также добавим кнопку «Участвовать» рядом с каждым опросом, если пользователь аутентифицирован:
опросы / index.html.erb
 [...]
def show
  @poll = Poll.find_by_id(params[:id])
end
[...]
Потребуется новый метод контроллера:
polls_controller.rb
 votes
Система аутентификации готова, и пришло время приступить к разработке функций голосования.
голосование
Как мы обсуждали ранее, необходимо установить связь между пользователем и опциями голосования, чтобы отслеживать, какой пользователь выбрал какую опцию. Каждый пользователь может голосовать за множество вариантов (однако он не может голосовать за несколько вариантов, принадлежащих одному опросу), и каждый вариант может быть выбран многими пользователями. Следовательно, нам нужна взаимосвязь «многие ко многим» с промежуточной таблицей (прямое отношение «многие ко многим» также может быть использовано, но оно не такое гибкое).
  Давайте назовем эту новую промежуточную таблицу $ rails g model Vote user:references vote_option:references 
 [...]
add_index :votes, [:vote_option_id, :user_id], unique: true
[...]
Затем немного измените файл миграции:
Миграции / xxx_create_votes.rb
 vote_option_id
  Это создаст кластеризованный индекс, который обеспечивает уникальность для комбинации user_id$ rake db:migrate  Очевидно, что один и тот же пользователь не может получить несколько голосов за один и тот же вариант. 
Затем примените эту миграцию:
 class Vote < ActiveRecord::Base
  belongs_to :user
  belongs_to :vote_option
end
Ваш файл модели должен выглядеть так:
модели / vote.rb
 User
  Добавьте эти ассоциации в модели VoteOption[...] 
has_many :votes, dependent: :destroy
has_many :vote_options, through: :votes
[...]
модели / user.rb
 [...]
has_many :votes, dependent: :destroy
has_many :users, through: :votes
[...]
модели / vote_option.rb
 show
  Создайте представление для действия <% content_for(:page_header) {"Share your opinion"} %>
<h2><%= @poll.topic %></h2>
<%= render 'voting_form' %>
опросы / show.html.erb
 <%= form_tag votes_path, method: :post, remote: true, id: 'voting_form' do %>
  <%= hidden_field_tag 'poll[id]', @poll.id %>
  <%= render partial: 'polls/vote_option', collection: @poll.vote_options, as: :option %>
  <% if current_user.voted_for?(@poll) %>
    <p>You have already voted!</p>
  <% else %>
    <%= submit_tag 'Vote', class: 'btn btn-lg btn-primary' %>
  <% end %>
<% end %>
Фактическая форма голосования взята на отдельную часть — это скоро пригодится.
опросы / _voting_form.html.erb
 votes_path
  Здесь мы используем несуществующий маршрут voted_for?  Эта форма будет отправлена асинхронно.  @poll.vote_options  Метод проверяет, участвовал ли уже пользователь в указанном опросе — он будет создан в ближайшее время. 
  Как вы можете видеть, мы используем [...]загруженную загрузку : 
def show
  @poll = Poll.includes(:vote_options).find_by_id(params[:id])
end
[...]
polls_controller.rb
 [...]
resources :votes, only: [:create]
[...]
Добавить новый маршрут:
routes.rb
 <div class="form-group">
  <%= content_tag(:label) do %>
    <% unless current_user.voted_for?(@poll) %>
      <%= radio_button_tag 'vote_option[id]', option.id %>
    <% end %>
    <%= option.title %>
  <% end %>
</div>
и создайте частичное:
опросы / _vote_option.html.erb
 content_tag
  labelvoted_for?  Переключатель не отображается, если пользователь уже участвовал в опросе. 
  Настало время реализовать [...]  метод: 
def voted_for?(poll)
  vote_options.any? {|v| v.poll == poll }
end
[...]
модели / user.rb
 def voted_for?(poll)
  Rails.cache.fetch('user_' + id.to_s + '_voted_for_' + poll.id.to_s) { vote_options.any? {|v| v.poll == poll } }
end
Здесь мы проверяем, есть ли у пользователя какие-либо варианты голосования, относящиеся к указанному опросу. Позже вы можете использовать кэширование моделей для повышения производительности приложения, например:
 create
и очищать этот кэш каждый раз, когда пользователь участвует в опросе.
  Наконец, нам нужен контроллер и метод class VotesController < ApplicationController 
  def create
    if current_user && params[:poll] && params[:poll][:id] && params[:vote_option] && params[:vote_option][:id]
      @poll = Poll.find_by_id(params[:poll][:id])
      @option = @poll.vote_options.find_by_id(params[:vote_option][:id])
      if @option && @poll && !current_user.voted_for?(@poll)
        @option.votes.create({user_id: current_user.id})
      else
        render js: 'alert(\'Your vote cannot be saved. Have you already voted?\');'
      end
    else
      render js: 'alert(\'Your vote cannot be saved.\');'
    end
  end
end
votes_controller.rb
 $('#voting_form').replaceWith('<%= j render 'polls/voting_form' %>');
Здесь проверьте, что все необходимые параметры были отправлены, что пользователь прошел проверку подлинности, и пользователь еще не участвовал в опросе. Если все эти условия выполняются, создайте новое голосование. В противном случае, показать предупреждение с сообщением об ошибке. На взгляд:
голосов / create.js.erb
 show
Здесь замените старую форму голосования новой, используя ранее созданную часть.
Давайте добавим немного визуализации для наших опросов.
Отображение статистики голосования
  В настоящее время на странице <div class="form-group"> 
  <%= content_tag(:label) do %>
    <% unless current_user.voted_for?(@poll) %>
      <%= radio_button_tag 'vote_option[id]', option.id %>
    <% end %>
    <%= option.title %>
  <% end %>
  <%= visualize_votes_for option %>
</div>
  Действительно, это нужно исправить!  Однако показывать только количество голосов скучно, поэтому давайте представим статистику, используя преимущества стилей Bootstrap: 
опросы / _vote_option.html.erb
 visualize_votes_for
  Здесь я добавил только одну строку, вызывающую module PollsHelper 
  def visualize_votes_for(option)
    content_tag :div, class: 'progress' do
      content_tag :div, class: 'progress-bar',
                  style: "width: #{option.poll.normalized_votes_for(option)}%" do
        "#{option.votes.count}"
      end
    end
  end
end
хелперы / polls_helper.rb
 div
  Оберните progress-bardiv.progressdiv.progress-bar  Эти классы предоставляются 
  Bootstrap и изначально предназначались для отображения индикаторов выполнения , но я думаю, что мы можем использовать их и в этом случае. 
  stylenormalized_votes_for  Ширина указана в процентах и, очевидно, должна быть не более 100%.  Чтобы убедиться в этом, я использую метод [...] 
def normalized_votes_for(option)
  votes_summary == 0 ? 0 : (option.votes.count.to_f / votes_summary) * 100
end
[...]
модели / poll.rb
 votes_summary
  Прежде всего, убедитесь, что countoption.votes.count  Так что, если нет голосов за какой-либо вариант опроса, просто верните ноль.  В противном случае проверьте, сколько голосов было отдано за указанную опцию, используя метод to_f  Этот результат делится на общее количество голосов, а затем умножается на 100, чтобы преобразовать его в проценты. 
  Обратите внимание, что votes_summary[...]  В противном случае результатом деления всегда будет целое число. 
def votes_summary
  vote_options.inject(0) {|summary, option| summary + option.votes.count}
end
[...]
Этот метод прост. Например, если опрос имеет 10 голосов, вариант A имеет 3 голоса, а вариант B имеет 7 голосов:
- Вариант А : (3/10) * 100 = 30 (%)
 - Вариант B : (7/10) * 100 = 70 (%)
 
  Большой!  И наконец, нам нужен метод inject 
модели / poll.rb
 0
  Здесь, summary используется для накопления значений (здесь [...]
.progress {
  background-image: linear-gradient(to bottom, #bbb 0%, #ccc 100%)
}
[...]votes_summary  Еще раз, вы можете использовать модель кэширования для повышения производительности. 
Последнее, что нужно сделать, это изменить фон индикатора выполнения, чтобы он выглядел немного лучше:
application.css.scss
 <%= form_tag votes_path, method: :post, remote: true, id: 'voting_form' do %>
  <%= hidden_field_tag 'poll[id]', @poll.id %>
  <%= render partial: 'polls/vote_option', collection: @poll.vote_options, as: :option %>
  <p><b>Total votes: <%= @poll.votes_summary %></b></p>
[...]
  Этот метод [...] 
<% @polls.each do |poll| %>
  <div class="well">
    <h2><%= poll.topic %> <small>(voted: <%= poll.votes_summary %>)</small></h2>
[...]
опросы / _voting_form.html.erb
 voted_for?
опросы / index.html.erb
 _voting_form
  Прежде чем двигаться дальше, я должен предупредить вас о небольшом подвохе.  Если вы запустите сервер и попытаетесь проголосовать в опросе, счетчик голосов обновится, но кнопка «Голосовать» останется.  Обновите страницу, и кнопка «Голосование» будет заменена текстом «Вы уже проголосовали».  Это происходит потому, что Rails кэшировал ассоциацию в false  метод.  Когда часть reset[...]  Существует как минимум три возможных решения этой проблемы. 
def create
  [...]
  @option.votes.create({user_id: current_user.id})  + current_user.votes.create({vote_option_id: @option.id})
  current_user.vote_options.reset
[...]
  Первый — просто очистить кэш ассоциации после создания нового голосования, используя метод voted_for? 
votes_controller.rb
 [...]
def voted_for?(poll)
  votes.any? {|v| v.vote_option.poll == poll}
end
[...]
  Второй переписывает force_reload  метод немного: 
user.rb
 [...]
def voted_for?(poll)
  vote_options(true).any? {|v| v.poll == poll }
end
[...]
Таким образом, мы напрямую указываем промежуточную модель, и Rails мгновенно узнает, что было создано новое голосование.
  Третье решение — установить option.votes.count 
user.rb
 [...]
gem 'counter_culture', '~> 0.1.23'
[...]
Если вы знаете другие решения этой проблемы, пожалуйста, поделитесь ими в комментариях.
Немного кеширования
  Возможно, вы заметили, что bundle install  Как насчет добавления кэша счетчика для улучшения этого запроса? 
Одним из возможных способов решения этой проблемы является использование драгоценного камня counter_culture, созданного Магнусом фон Келлером.
Бросьте драгоценный камень в свой Gemfile:
Gemfile
 $ rails g counter_culture VoteOption votes_count
$ rake db:migrate
  и запустите counter_culture :vote_option 
Далее запустите генератор и примените созданную миграцию:
 [...]
//= require jquery.validate
[...]
Также добавьте следующую строку кода в * voice.rb *:
 validate()
Теперь кеш счетчика для подсчета голосов будет создан и управляться автоматически. Круто, не правда ли?
Драгоценный камень counter_culture может использоваться в более сложных сценариях и имеет множество опций, поэтому ознакомьтесь с его документацией.
Немного о проверке на стороне клиента
Чтобы немного улучшить взаимодействие с пользователем, добавьте некоторую проверку на стороне клиента, чтобы проверить, выбрал ли пользователь один из вариантов перед голосованием. Если нет — покажите ошибку вместо отправки формы. Существует отличный плагин jQuery под названием jQuery Validate, который, как следует из названия, помогает создавать различные правила проверки. Просто скачайте файлы плагина, поместите их в vendor / assets / javascripts и включите в ваш проект jquery.validate.js и Additional-method.js :
application.js
 [...]
<script data-turbolinks-track="true">
  $(document).ready(function() {
    var voting_form = $('#voting_form');
    voting_form.validate({
      focusInvalid: false,
      errorClass: 'alert alert-warning',
      errorElement: "div",
      errorPlacement: function(error, element) { voting_form.before(error); },
      rules: {
        'vote_option[id]': {
          required: true
        }
      },
      messages: {
        'vote_option[id]': {
          required: "Please select one of the options."
        }
      }
    });
  });
</script>
  В простейшем случае все, что вам нужно сделать, это использовать метод focusInvalid  Однако у нас есть немного более сложный сценарий, поэтому мы должны предоставить несколько вариантов: 
опросы / show.html.erb
 errorClass
  errorElement  errorPlacement  beforeerror  Я хочу, чтобы он был помещен перед формой, поэтому я использую метод rulesvote_option[id] 
  messages  До тех пор, пока мы хотим убедиться, что установлен только один из class UsersController < ApplicationController
  def show
    @user = User.find_by_id(params[:id])
  end
end<% content_for(:page_header) do %>
  <%= image_tag @user.image_url, alt: @user.name %>
  <%= "#{@user.name}'s profile" %>
<% end %>
<h2>Participation in polls</h2>
<% @user.vote_options.each do |option| %>
  <div class="panel panel-default">
    <div class="panel-heading"><%= link_to option.poll.topic, poll_path(option.poll) %></div>
    <div class="panel-body">
      <%= option.title %>
    </div>
  </div>
<% end %>[...]  
def show
  @user = User.includes(:vote_options).find_by_id(params[:id])
end
[...][...] 
resources :users, only: [:show]
[...]
На этом этапе вы можете проверить, как работает валидация, просто попытавшись отправить форму без включенных переключателей. Потрясающие!
Отображение профиля пользователя
Мы сделали с основной функциональностью. Пользователи могут участвовать в опросах и проверять статистику. Последнее, что нужно сделать, это создать страницу профиля пользователя, чтобы показать опросы, в которых он участвовал. Это можно сделать за три простых шага: создать контроллер, создать представление и добавить новый маршрут.
users_controller.rb
 [...]
<ul class="nav navbar-nav navbar-right">
  <% if current_user %>
    <li><%= image_tag current_user.image_url, alt: current_user.name %></li>
    <li><%= link_to 'Profile', user_path(current_user) %></li>
    <li><%= link_to 'Logout', sign_out_path %></li>
  <% else %>
    <li><%= link_to 'Sign in', '/auth/facebook' %></li>
  <% end %>
</ul>
[...]
пользователи / show.html.erb
 end
Возьмите вариант голосования каждого пользователя и отобразите его вместе с темой опроса. Используйте готовую загрузку здесь, чтобы улучшить производительность:
users_controller.rb
 do … end
Не забудьте настроить маршрут:
routes.rb
 {}
Последнее, что нужно сделать, это предоставить ссылку на профиль пользователя:
макеты / application.html.erb
 bundle install
Загрузите свой сервер и проверьте его!
Вывод
При создании этого приложения Poller мы обсуждали nested_attributes, драгоценный камень Cocoon, рассмотрели нормализацию подсчета голосов и обсудили некоторые вопросы кэширования, такие как кэширование модели и использование драгоценного камня counter_culture. Кроме того, мы рассмотрели плагин jQuery Validate и некоторые модные стили Bootstrap. Думаю, на сегодня этого достаточно!
Конечно, это приложение можно улучшить, добавив больше кэширования, возможность повторного голосования или отмены голосования, немного его стилизации и т. Д. Не стесняйтесь клонировать мой репозиторий с демо-кодом и экспериментировать с ним!
Надеюсь, вы нашли эту статью полезной и интересной (я должен создать опрос, чтобы выяснить это?). Спасибо, что остаетесь со мной до конца и до скорой встречи!
