Статьи

Нажмите Интересы пользователя с курируемыми лентами в Rails

Часто при регистрации на новой платформе вас просят выбрать из списка элементы, чтобы увидеть сообщения, которые соответствуют вашим интересам. Некоторые из сайтов, использующих эту технику, это Quora , Medium и pinterest, и это лишь некоторые из них. Это скриншот из quora сразу после регистрации.

Пример курируемой подачи

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

Давайте начнем с создания нового приложения rails. Я использую рельсы 4.2 в этом уроке:

rails new curated-feed 

Блог

Теперь давайте создадим контроллер сообщений и модель сообщений соответственно:

 rails g controller Posts index feed new edit show rails g model Posts title:string description:text rake db:migrate 

Посты, которые мы будем создавать, довольно простые, имеют только заголовок и описание. Давайте добавим ресурс постов, чтобы мы могли получить все маршруты для выполнения операций CRUD над постами . Мы также изменим наш root_path чтобы он указывал на действие index в PostsController :

конфиг / routes.rb

 Rails.application.routes.draw do #create routes for performing all CRUD operations on posts resources :posts #make the homepage the index action of the posts controller root 'posts#index' end 

Теперь добавьте следующий код в действия контроллера:

приложение / контроллеры / posts_controller.rb

 class PostsController < ApplicationController before_action :find_post, only: [:show, :edit, :update, :destroy] def index @posts = Post.all end def feed end def new @post = Post.new end def create @post = Post.new post_params if @post.save flash[:success] = "Post was created successfully" redirect_to @post else render :new end end def show end def edit end def update if @post.update post_params flash[:success] = "The post was updated successfully" redirect_to @post else flash.now[:danger] = "Error while submitting post" render :edit end end def destroy @post.destroy redirect_to root_url end private def post_params params.require(:post).permit(:title, :description, tag_ids: []) end def find_post @post = Post.find(params[:id]) end end 

Действия index , create , update и destroy являются обычными для типичного приложения CRUD на Rails. Мы объявили before_action чтобы найти сообщение для show и edit действия. Мы добавим код для метода feed позже.

На создание взглядов. Мы будем использовать bootstrap, поскольку он делает стилизацию довольно простой:

Gemfile

 gem 'bootstrap-sass' [...] 

Затем запустите bundle install

Нам нужно обновить наш файл app / files / css / application.scss, чтобы стили загрузки могли вступить в силу:

 @import "bootstrap"; @import "bootstrap-sprockets"; 

Во-первых, вот код для нашего макета:

приложение / просмотров / макеты / application.html.erb

 <nav class="navbar navbar-default"> <div class="container"> <div class="navbar-header"> <%= link_to 'Curated-feed', root_path, class: 'navbar-brand' %> </div> <div id="navbar"> <ul class="nav navbar-nav pull-right"> <li> </ul> </div> </div> </nav> <div class="container"> <% flash.each do |key, value| %> <div class="alert alert- <%= value %> </div> <% end %> <%= yield %> </div> 

Всегда хорошая идея сделать частичное для формы:

приложение / просмотров / сообщений / _form.html.erb

 <div class="form-group"> <%= form_for @post do |f| %> <div class="form-group"> <%= f.label :title %> <%= f.text_field :title, class: "form-control" %> </div> <div class="form-group"> <%= f.label :description %> <%= f.text_area :description, class: "form-control" %> </div> <div class="form-group"> <%= f.submit class: "btn btn-primary" %> </div> <% end %> </div> 

Наконец, вот new , edit , show и index представления для сообщения:

приложение / просмотров / сообщений / new.html.erb

 <div class="col-md-6 col-md-offset-3"> <h1>Create a new post</h1> <%= render 'form' %> </div> 

приложение / просмотров / сообщений / edit.html.erb

 <div class="col-md-6 col-md-offset-3"> <h1>Create a new post</h1> <%= render 'form' %> </div> 

приложение / просмотров / сообщений / show.html.erb

 <div class="col-md-8 col-md-offset-2"> <h1><%= @post.title %></h1> <p><%= @post.description %></p> </div> 

приложение / просмотров / сообщений / index.html.erb

 <div class="col-md-8 col-md-offset-2"> <h1>All posts</h1> <% @posts.each do |post| %> <ul> <li> <h3><%= post.title %></h3> <p><%= post.description %></p> </li> </ul> <% end %> </div> 

Итак, до сих пор мы оставались в рамках RESTful подхода Rails. Вот представление канала, которое является нестандартным.

приложение / просмотров / сообщений / feed.html.erb

 <div class="col-md-8 col-md-offset-2"> <h1>Feed</h1> </div> 

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

Теперь, когда мы настроили наши представления, мы можем заполнить нашу базу данных, чтобы увидеть общий макет. Я собираюсь заполнить базу данных 15 сообщениями, используя гем faker , так что добавьте это в Gemfile :

 [...] gem 'faker', '~> 1.6', '>= 1.6.6' [...] 

Запустите bundle install чтобы установить гем. С жемчужиной в нашем наборе инструментов начальный файл выглядит следующим образом:

приложение / дб / seeds.rb

 15.times do title = Faker::Lorem.sentence # all options available below description = Faker::Lorem.paragraph Post.create!(title: title, description: description) end 

Запустите rake db:seed чтобы заполнить нашу базу данных поддельными данными. Если вы посетите http://localhost:3000 в вашем браузере, вы должны увидеть случайные сообщения. Это было скучно, но я уверен, что вы уже прошли создание блога с уровнем Rails. В следующем разделе мы поговорим о добавлении тегов к сообщениям.

Пометка постов

Теги и сообщения, вы, вероятно, понимаете отношение «многие ко многим», то есть тег может быть связан со многими сообщениями и наоборот. Для этого нам понадобится сводная таблица. Сводная таблица — это промежуточная таблица со связями между двумя другими таблицами. Он связывает две таблицы вместе, используя два внешних ключа для определения строк из других таблиц. Диаграмма ниже поможет вам лучше понять это:

Диаграмма сводной таблицы

Для начала сгенерируйте модель Tag:

 rails g model Tag title rake db:migrate 

И опорная модель. Давайте назовем это post_tag :

 rails g model Post_tag title post:references tag:references rake db:migrate 

Обновите наши модели, чтобы принять к сведению отношения:

приложение / модели / tag.rb

 [...] has_many :post_tags, dependent: :destroy has_many :posts, through: :post_tags [...] 

приложение / модели / post.rb

 [...] has_many :post_tags, dependent: :destroy has_many :tags, through: :post_tags [...] 

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

приложение / контроллеры / posts_controller.rb

 def post_params params.require(:post).permit(:title, :description, tag_ids: []) end 

Обратите внимание, что tag_ids хранятся в виде массива, поскольку сообщение может быть помечено несколькими тегами. Мы не будем создавать контроллер тегов, так как все теги создаются в консоли для этого урока. Тем не менее, мы создадим частичный тег, что позволит нам отображать теги под сообщениями:

 mkdir app/views/tags touch app/views/tags/_tag.html.erb 

приложение / просмотров / теги / tag.html.erb

 <span class="quiet"><small><%= link_to tag.title, "#" %> </small></span> 

Давайте создадим несколько тегов через консоль. Пять будет делать:

 $ rails c Tag.create!(title: "technology") Tag.create!(title: "politics") Tag.create!(title: "science") Tag.create!(title: "entrepreneurship") Tag.create!(title: "programming") 

Мы могли бы использовать флажки для пометки сообщений, но флажки могут быть ограничивающими. Допустим, у нас были сотни тегов, то есть сотни флажков. Грязный, верно? В этом уроке нам понадобится камень для выбранных рельсов . Chosen — это плагин jQuery, который делает длинные, громоздкие поля выбора более удобными для пользователя. Это также позволяет фильтровать по тегам, просто введя имя тега. Добавьте это в ваш Gemfile и выполните bundle install :

Gemfile

 [...] gem 'compass-rails' gem 'chosen-rails' [...] 

Обновите следующие файлы, чтобы сделать выбранный эффект эффективным:

приложение / активы / JavaScripts / application.js

 [...] //Make sure you require chosen after jquery. In my case I have it after the turbolinks line //= require chosen-jquery [...]
 Это для тех, кто использует JQuery.  Есть документация для тех из вас, кто использует прототип на github repo с выбранными рельсами .

 Приложение / активы / CSS / application.scss
 [...] *= require chosen [...] 

Затем в папке app / assets / javascripts добавьте этот файл CoffeeScript:

приложение / активы / JavaScripts / tag-select.js.coffee

  $ -> # enable chosen js $('.chosen-select').chosen allow_single_deselect: true no_results_text: 'No results matched' width: '450px' 

Добавьте этот фрагмент внутри формы, чтобы пользователи могли выбирать теги:
приложение / просмотров / сообщений / _form.html.erb

 <div class="form-group"> <%= f.collection_select :tag_ids, Tag.order(:title), :id, :title, {}, { multiple: true, class: "chosen-select" } %> </div> </code></pre> <p>This is how your form should look like now:</p> <pre><code> <div class="form-group"> <%= form_for @post do |f| %> <div class="form-group"> <%= f.label :title %> <%= f.text_field :title, class: "form-control" %> </div> <div class="form-group"> <%= f.label :description %> <%= f.text_area :description, class: "form-control" %> </div> <div class="form-group"> <%= f.collection_select :tag_ids, Tag.order(:title), :id, :title, {}, { multiple: true, class: "chosen-select" } %> </div> <div class="form-group"> <%= f.submit class: "btn btn-primary" %> </div> <% end %> </div> 

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

приложение / просмотров / сообщений / index.html.erb

 <div class="col-md-8 col-md-offset-2"> <h1>All posts</h1> <% @posts.each do |post| %> <ul> <li> <h3><%= post.title %></h3> <p><%= post.description %></p> <% if post.tags.any? %> <p>Tags: <%= render post.tags %></p> <% end %> </li> </ul> <% end %> </div> 

Моделирование пользователей

Давайте позволим людям зарегистрироваться на нашей платформе. Для этого мы будем использовать самоцвет для разработки :

Gemfile

 [...] gem 'devise', '~> 4.2' [...] 

Затем выполните bundle install .

Запустите генератор устройства.

 rails generate devise:install 

После того, как вы выполните эту команду, вы увидите пару инструкций в консоли. Вы можете спокойно игнорировать эти инструкции, так как мы настроили большинство вещей и не будем отправлять электронные письма сегодня.

Давайте сгенерируем модель User:

 rails g devise User rake db:migrate 

На этом этапе пользователи могут зарегистрироваться и войти. Обновите навигацию, чтобы пользователи могли видеть ссылки регистрации , входа и выхода :

приложение / просмотров / макеты / application.html.erb

 <nav class="navbar navbar-default"> <div class="container"> <div class="navbar-header"> <%= link<em>to 'Curated-feed', root</em>path, class: 'navbar-brand' %> </div> <div id="navbar"> <ul class="nav navbar-nav pull-right"> <% unless user_signed_in? %> <li><%= link_to "Sign in", new_user_session_path %></li> <li><%= link_to "Sign up", new_user_registration_path %></li> <% else %> <li><%= link_to "Sign out", destroy_user_session_path, method: :delete %></li> <% end %> </ul> </div> </div> </nav> <div class="container"> <% flash.each do |key, value| %> <div class="alert alert-<%= key %>"> <%= value %> </div> <% end %> <%= yield %> </div> 

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

 rails g model user_tag user:references tag:references rake db:migrate 

Обновите Tag и User модели. Теперь они должны выглядеть так:

приложение / модели / tag.rb

 class Tag < ActiveRecord::Base has_many :post_tags, dependent: :destroy has_many :posts, through: :post_tags has_many :user_tags, dependent: :destroy has_many :users, through: :user_tags end 

приложение / модели / user.rb

 class User < ActiveRecord::Base # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable has_many :user_tags, dependent: :destroy has_many :tags, through: :user_tags end 

Создайте пользовательский контроллер:

 rails g controller users edit update 

Обратите внимание, что new и создаваемые действия позаботятся о Devise. Devise также предоставляет метод для обновления объекта пользователя без предоставления пароля, если вы редактируете атрибуты внутри UsersController . В нашем случае мы передадим tag_ids как часть параметров, которые мы хотим обновить:

приложение / контроллеры / users_controller.rb

 class UsersController < ApplicationController before_action :find_user def edit end def update if @user.update(user_params) flash[:success] = "Interests updated" redirect_to root_path else flash[:alert] = "Interests could not be updated." render :edit end end private def find_user @user = current_user end def user_params params.require(:user).permit(tag_ids: []) end end 

Теперь мы можем назначить tag_ids пользователю через консоль. Давайте обновим наш файл маршрутов, чтобы приспособить действия по edit и update пользователя. Мы также создадим представление для обновления интересов пользователей и добавим ссылку для обновления интересов в навигации:

конфиг / routes.rb

 [...] resources :users, only: [:edit, :update] [...] 

приложение / просмотров / пользователей / edit.html.erb

 <div class="col-md-8 col-md-offset-2"> <h1>Please check your interests</h1> <%= form_for (@user) do |f| %> <strong>Interests:</strong> <%= f.collection_check_boxes :tag_ids, Tag.all, :id, :title do |cb| %> <%= cb.label(class: "checkbox-inline input_checkbox") {cb.check_box(class: "checkbox") + cb.text} %> <% end %> <br><br> <div class="form-group"> <%= f.submit class: "button button_flat button_block" %> </div> <% end %> </div> 

приложение / просмотров / макеты / application.html.erb

 [...] <% unless user_signed_in? %> <li><%= link_to "Sign in", new_user_session_path %></li> <li><%= link_to "Sign up", new_user_registration_path %></li> <% else %> <li><%= link_to "Update interests", edit_user_path(current_user) %></li> <li><%= link_to "Sign out", destroy_user_session_path, method: :delete %></li> <% end %> [...] 

Посетите http: // localhost: 3000 / users / 1 / edit и вы увидите список интересов с флажками рядом с ними. В представлении редактирования мы используем именованные интересы при обращении к тегам. Однако в большинстве случаев пользователи должны выбирать свои интересы сразу после регистрации перед доступом к доступному контенту.

Давайте создадим новый контроллер с именем registrations_controller и добавим код для перенаправления пользователей в представление редактирования после регистрации:

 touch app/controllers/registrations_controller.rb 

приложение / контроллеры / registrations_controller.rb

 class RegistrationsController < Devise::RegistrationsController protected def after_sign_up_path_for(resource) edit_user_path(current_user) end end 

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

конфиг / routes.rb

 [...] devise_for :users, controllers: { registrations: "registrations" } [...] 

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

Мы еще не обновили код в методе feed внутри posts_controller . Тем временем пользователи будут просто перенаправлены на root_path . Давайте обновим наш метод подачи сейчас:

приложение / контроллеры / posts_controller.rb

 def feed @my_interests = current_user.tag_ids @posts = Post.select { |p| (p.tag_ids & @my_interests).any? } end 

В первой строке мы получаем все tag_ids принадлежащие аутентифицированному пользователю — помните, что мы сохранили tag_ids в виде массива. В Ruby мы можем искать пересечение между двумя массивами, используя символ & . Запустите нашу консоль, чтобы вы поняли, что я имею в виду:

 rails c irb(main):001:0> [1,2,3,4] & [3, 1] => [1, 3] #what is returned irb(main):002:0> [7,3,4,5] & [3, 1] => [3] irb(main):003:0> [2,3,4,5] & [6, 7] => [] 

Если есть пересечение между двумя массивами, возвращается массив, содержащий элементы, общие для двух массивов, в противном случае возвращается пустой массив. Давайте вернемся к коду внутри метода feed . Как только мы получим tag_ids принадлежащие аутентифицированному пользователю, мы можем сравнить эти tag_ids с tag_ids принадлежащим сообщению. Если есть пересечение, сообщение будет выбрано и добавлено в переменную @posts .

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

приложение / просмотров / сообщений / feed.html.erb

 <div class="col-md-8 col-md-offset-2"> <h1>Your feed</h1> <% @posts.each do |post| %> <ul> <li> <h3><%= post.title %></h3> <p><%= post.description %></p> <% if post.tags.any? %> <p>Tags: <%= render post.tags %></p> <% end %> </li> </ul> <% end %> </div> 

Тем не менее, посещение главной страницы показывает все сообщения, независимо от того, отмечены ли они какими-либо интересами пользователей или нет. Это потому, что root_path сопоставлен с posts#index . Прошедшие проверку пользователи должны видеть только соответствующие сообщения, а не все. Devise поставляется с несколькими методами, в том числе с возможностью определения root_path для аутентифицированных пользователей. В config / rout.Rb добавить это:

конфиг / routes.rb

  [...] authenticated :user do root 'posts#feed', as: "authenticated_root" end root 'posts#index' [...] 

Теперь, когда мы заходим на http: // localhost: 3000 , будут отображаться только сообщения, отмеченные нашими интересами. Тогда есть проблема аутентифицированного пользователя, не выбравшего какие-либо интересы. Значит ли это, что их корм будет пустым? Давайте вернемся к методу feed внутри posts_controller и обновим его:

 def feed @my_interests = current_user.tag_ids #check if the authenticated user has any interests if @my_interests.any? @posts = Post.select { |p| (p.tag_ids & @my_interests).any? } else #load all the posts if the authenticated user has not specified any interests @posts = Post.all end end 

Вывод

Это простой подход к тому, как это сделать. Я хотел поговорить об отношениях «многие ко многим» и о том, как проверять пересечения между различными моделями. Вы можете построить поверх этого, добавив изображения в теги, а затем отображая изображения рядом с флажками в представлении. Вот как это делают большинство сайтов. Вы также можете позволить пользователям нажимать на изображения вместо флажков, делая AJAX-вызовы для обновления объекта пользователя. Есть так много способов сделать это модным. Я изучаю Rails и люблю делиться тем, что я узнал, поэтому я надеюсь, что этот урок был полезным