Beta polls feature (admin use only for now) (#3176)

* Initial poll features

* Add basic poll functionality

* Finish (maybe) beta poll functionality
This commit is contained in:
Ben Halpern 2019-06-15 17:33:30 -04:00 committed by GitHub
parent f29b67a7b6
commit 8ae057b06e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 722 additions and 4 deletions

View file

@ -10,3 +10,4 @@
@import 'InstagramTag';
@import 'GistTag';
@import 'GithubReadmeTag';
@import 'PollTag';

View file

@ -0,0 +1,85 @@
@import 'variables';
.ltag-poll {
width: 600px;
max-width: 96%;
margin: auto;
border-radius: 3px;
border:1px solid $light-medium-gray !important;
box-shadow: $shadow;
box-sizing: border-box;
padding: 2px 15px 18px;
font-family: $helvetica;
margin:1.6em auto !important;
h3 {
margin: 15px 0px;
}
.ltag-pollanswers {
list-style-type: none !important;
margin: 0 !important; /* To remove default bottom margin */
padding: 0 !important;
padding-bottom: 8px !important;
font-size: 0.9em !important;
li {
padding-left: 8px;
border-radius: 3px;
width: 95%;
position: relative;
&:hover {
background: $light-gray;
}
&.ltag-polloption-justshowmetheresults:hover {
background: transparent;
}
&.already-voted {
background: transparent;
}
}
input {
vertical-align: top;
margin-top: 1.05em;
}
label {
width: calc(100% - 40px);
padding: 2px 6px;
cursor: pointer;
vertical-align: -3px;
display: inline-block;
line-height: 1.25em;
}
.ltag-voting-results-count {
font-size: 0.7em;
color: $medium-gray;
text-align: left;
}
button {
width: 100%;
background: transparent;
border: 0px;
font-size: 0.7em;
color: $medium-gray;
text-align: left;
padding-left: 20px;
}
.ltag-votepercent {
display: inline-block;
position: absolute;
top: 0;
left: 0;
bottom: 0;
z-index: 3;
border-radius: 3px;
&.ltag-optionvotedfor {
background: $green;
font-weight: bold;
}
&.ltag-optionnotvotedfor {
background: $light-medium-gray;
}
}
.ltag-votepercenttext {
position: relative;
z-index: 5;
}
}
}

View file

@ -0,0 +1,19 @@
class PollSkipsController < ApplicationController
before_action :authenticate_user!, only: %i[create]
def create
@poll_skip = PollSkip.create(poll_id: poll_skips_params[:poll_id], user_id: current_user.id)
@poll = Poll.find(poll_skips_params[:poll_id])
render json: { voting_data: @poll.voting_data,
poll_id: poll_skips_params[:poll_id].to_i,
user_vote_poll_option_id: nil,
voted: false }
end
private
def poll_skips_params
accessible = %i[poll_id]
params.require(:poll_skip).permit(accessible)
end
end

View file

@ -0,0 +1,30 @@
class PollVotesController < ApplicationController
before_action :authenticate_user!, only: %i[create]
def show
@poll = Poll.find(params[:id]) #Querying the poll instead of the poll vote
@poll_vote = @poll.poll_votes.where(user_id: current_user).first
@poll_skip = @poll.poll_skips.where(user_id: current_user).first if @poll_vote.blank?
render json: { voting_data: @poll.voting_data,
poll_id: @poll.id,
user_vote_poll_option_id: @poll_vote&.poll_option_id,
voted: (@poll_vote || @poll_skip).present? }
end
def create
@poll_option = PollOption.find(poll_vote_params[:poll_option_id])
@poll_vote = PollVote.create(poll_option_id: @poll_option&.id, user_id: current_user.id, poll_id: @poll_option.poll_id)
@poll = @poll_option.reload.poll
render json: { voting_data: @poll.voting_data,
poll_id: @poll.id,
user_vote_poll_option_id: poll_vote_params[:poll_option_id].to_i,
voted: true }
end
private
def poll_vote_params
accessible = %i[poll_option_id]
params.require(:poll_vote).permit(accessible)
end
end

View file

@ -25,10 +25,6 @@ class ArticleDecorator < ApplicationDecorator
"https://#{ApplicationConfig['APP_DOMAIN']}#{path}"
end
def liquid_tags_used
MarkdownParser.new(body_markdown.to_s + comments_blob.to_s).tags_used
end
def title_length_classification
if article.title.size > 105
"longest"

View file

@ -58,6 +58,18 @@ class MarkdownParser
attributes: allowed_attributes
end
def evaluate_inline_limited_markdown
return if @content.blank?
renderer = Redcarpet::Render::HTMLRouge.new(hard_wrap: true, filter_html: false)
markdown = Redcarpet::Markdown.new(renderer, REDCARPET_CONFIG)
allowed_tags = %w[strong i u b em code]
allowed_attributes = %w[href strong em ref rel src title alt class]
ActionController::Base.helpers.sanitize markdown.render(@content).html_safe,
tags: allowed_tags,
attributes: allowed_attributes
end
def evaluate_listings_markdown
return if @content.blank?

121
app/liquid_tags/poll_tag.rb Normal file
View file

@ -0,0 +1,121 @@
class PollTag < LiquidTagBase
PARTIAL = "liquids/poll".freeze
def initialize(_tag_name, id_code, _tokens)
@poll = Poll.find(id_code)
end
def render(_context)
ActionController::Base.new.render_to_string(
partial: PARTIAL,
locals: {
poll: @poll
},
)
end
def find_poll(id_code)
Poll.find(id_code.to_i(26))
rescue ActiveRecord::RecordNotFound
raise StandardError, "Invalid poll ID"
end
def self.script
<<~JAVASCRIPT
if (document.head.querySelector(
'meta[name="user-signed-in"][content="true"]',
)) {
function displayPollResults(json) {
var totalVotes = json.voting_data.votes_count;
json.voting_data.votes_distribution.forEach(function(point) {
var pollOptionItem = document.getElementById('poll_option_list_item_'+point[0]);
var optionText = document.getElementById('poll_option_label_'+point[0]).textContent;
if (json.user_vote_poll_option_id === point[0]) {
var votedClass = 'optionvotedfor'
} else {
var votedClass = 'optionnotvotedfor'
}
if (totalVotes === 0) {
var percent = 0;
} else {
var percent = (point[1]/totalVotes)*100;
}
var roundedPercent = Math.round( percent * 10 ) / 10
var percentFromRight = (100-roundedPercent)
var html = '<span><span class="ltag-votepercent ltag-'+votedClass+'" style="right:'+percentFromRight+'%"></span>\
<span class="ltag-votepercenttext">'+optionText+' '+roundedPercent+'%</span></span>';
pollOptionItem.innerHTML = html;
pollOptionItem.classList.add('already-voted')
document.getElementById('showmethemoney-'+json.poll_id).innerHTML = '<span class="ltag-voting-results-count">'+totalVotes+' total votes</span>';
})
}
var polls = document.getElementsByClassName('ltag-poll');
for (i = 0; i < polls.length; i += 1) {
var poll = polls[i]
var pollId = poll.dataset.pollId
window.fetch('/poll_votes/'+pollId)
.then(function(response){
response.json().then(
function(json){
if (json.voted) {
displayPollResults(json)
} else {
var els = document.getElementById('poll_'+json.poll_id).getElementsByClassName('ltag-polloption')
for (i = 0; i < els.length; i += 1) {
els[i].addEventListener('click', function(e) {
var tokenMeta = document.querySelector("meta[name='csrf-token']")
if (!tokenMeta) {
alert('Whoops. There was an error. Your vote was not counted. Try refreshing the page.')
return
}
var csrfToken = tokenMeta.getAttribute('content')
var optionId = e.target.dataset.optionId
window.fetch('/poll_votes', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({poll_vote: { poll_option_id: optionId } }),
credentials: 'same-origin',
}).then(function(response){
response.json().then(function(j){displayPollResults(j)})
})
});
}
document.getElementById('showmethemoney-'+json.poll_id).addEventListener('click', function(e) {
pollId = this.dataset.pollId
window.fetch('/poll_skips', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({poll_skip: {poll_id: pollId }}),
credentials: 'same-origin',
}).then(function(response){
response.json().then(function(j){displayPollResults(j)})
})
});
}
}
)
})
}
} else {
var els = document.getElementsByClassName('ltag-poll')
for (i = 0; i < els.length; i += 1) {
els[i].onclick = function(e) {
if (typeof showModal !== "undefined") {
showModal('poll');
}
}
}
}
JAVASCRIPT
end
end
Liquid::Template.register_tag("poll", PollTag)

View file

@ -42,6 +42,7 @@ class Article < ApplicationRecord
validate :validate_tag
validate :validate_video
validate :validate_collection_permission
validate :validate_liquid_tag_permissions
validates :video_state, inclusion: { in: %w[PROGRESSING COMPLETED] }, allow_nil: true
validates :cached_tag_list, length: { maximum: 86 }
validates :main_image, url: { allow_blank: true, schemes: %w[https http] }
@ -444,6 +445,10 @@ class Article < ApplicationRecord
Rails.logger.error(e)
end
def liquid_tags_used
MarkdownParser.new(body_markdown.to_s + comments_blob.to_s).tags_used
end
private
def update_notifications
@ -519,6 +524,10 @@ class Article < ApplicationRecord
errors.add(:collection_id, "must be one you have permission to post to") if collection && collection.user_id != user_id
end
def validate_liquid_tag_permissions #Admin only beta tags etc.
errors.add(:body_markdown, "must only use permitted tags") if liquid_tags_used.include?(PollTag) && !(user.has_role?(:super_admin) || user.has_role?(:admin))
end
def create_slug
if slug.blank? && title.present? && !published
self.slug = title_to_slug + "-temp-slug-#{rand(10_000_000)}"

35
app/models/poll.rb Normal file
View file

@ -0,0 +1,35 @@
class Poll < ApplicationRecord
attr_accessor :poll_options_input_array
serialize :voting_data
belongs_to :article
has_many :poll_options
has_many :poll_skips
has_many :poll_votes
validates :prompt_markdown, presence: true,
length: { maximum: 128 }
validates :poll_options_input_array, presence: true,
length: { minimum: 2, maximum: 15 }
after_create :create_poll_options
before_save :evaluate_markdown
def voting_data
{ votes_count: poll_votes_count, votes_distribution: poll_options.pluck(:id, :poll_votes_count) }
end
private
def create_poll_options
poll_options_input_array.each do |input|
PollOption.create!(markdown: input, poll_id: id)
end
end
def evaluate_markdown
self.prompt_html = MarkdownParser.new(prompt_markdown).evaluate_inline_limited_markdown
end
end

17
app/models/poll_option.rb Normal file
View file

@ -0,0 +1,17 @@
class PollOption < ApplicationRecord
belongs_to :poll
has_many :poll_votes
validates :markdown, presence: true,
length: { maximum: 128 }
before_save :evaluate_markdown
counter_culture :poll
private
def evaluate_markdown
self.processed_html = MarkdownParser.new(markdown).evaluate_inline_limited_markdown
end
end

12
app/models/poll_skip.rb Normal file
View file

@ -0,0 +1,12 @@
class PollSkip < ApplicationRecord
belongs_to :poll
belongs_to :user
validate :one_vote_per_poll_per_user
private
def one_vote_per_poll_per_user
errors.add(:base, "cannot vote more than once in one poll") if (poll.poll_votes.where(user_id: user_id).any? || poll.poll_skips.where(user_id: user_id).any?)
end
end

30
app/models/poll_vote.rb Normal file
View file

@ -0,0 +1,30 @@
class PollVote < ApplicationRecord
belongs_to :user
belongs_to :poll_option
belongs_to :poll
counter_culture :poll_option
counter_culture :poll
validates :poll_id, presence: true, presence: true, uniqueness: { scope: :user_id } # In the future we'll remove this constraint if/when we allow multi-answer polls
validates :poll_option_id, presence: true, uniqueness: { scope: :user_id }
validate :one_vote_per_poll_per_user
after_save :touch_poll_votes_count
after_destroy :touch_poll_votes_count
def poll
poll_option.poll
end
private
def one_vote_per_poll_per_user
errors.add(:base, "cannot vote more than once in one poll") if poll.poll_votes.where(user_id: user_id).any? || poll.poll_skips.where(user_id: user_id).any?
end
def touch_poll_votes_count
poll.update_column(:poll_votes_count, poll.poll_votes.size)
poll_option.update_column(:poll_votes_count, poll_option.poll_votes.size)
end
end

View file

@ -38,6 +38,8 @@ class User < ApplicationRecord
has_many :page_views
has_many :credits
has_many :classified_listings
has_many :poll_votes
has_many :poll_skips
has_many :mentor_relationships_as_mentee,
class_name: "MentorRelationship", foreign_key: "mentee_id", inverse_of: :mentee
has_many :mentor_relationships_as_mentor,

View file

@ -236,6 +236,7 @@
<%= PodcastTag.script.html_safe %>
<%= GistTag.script.html_safe %>
<%= RunkitTag.script.html_safe %>
<%= PollTag.script.html_safe %>
</script>
<% end %>

View file

@ -0,0 +1,12 @@
<div class="ltag-poll" id="poll_<%= poll.id %>" data-poll-id="<%= poll.id %>">
<h3><%= poll.prompt_html.html_safe %></h3>
<ul class="ltag-pollanswers">
<% poll.poll_options.each do |option| %>
<li class="ltag-polloption" id="poll_option_list_item_<%= option.id %>" data-option-id="<%= option.id %>">
<input type="radio" id="poll_option_<%= option.id %>" name="option" value="poll_option_<%= option.id %>" data-option-id="<%= option.id %>">
<label for="d" id="poll_option_label_<%= option.id %>" data-option-id="<%= option.id %>"><%= option.processed_html.html_safe %></label>
</li>
<% end %>
<li class="ltag-polloption-justshowmetheresults" id="showmethemoney-<%= poll.id %>" data-poll-id="<%= poll.id %>"><button>Just show me the results</button>
</ul>
</div>

View file

@ -143,6 +143,8 @@ Rails.application.routes.draw do
resources :credits, only: %i[index new create]
resources :buffer_updates, only: [:create]
resources :reading_list_items, only: [:update]
resources :poll_votes, only: %i[show create]
resources :poll_skips, only: [:create]
get "/chat_channel_memberships/find_by_chat_channel_id" => "chat_channel_memberships#find_by_chat_channel_id"
get "/credits/purchase" => "credits#new"

View file

@ -0,0 +1,14 @@
class CreatePolls < ActiveRecord::Migration[5.2]
def change
create_table :polls do |t|
t.bigint :article_id
t.string :prompt_markdown
t.string :prompt_html
t.boolean :allow_multiple_selections, default: false
t.integer :poll_options_count, null: false, default: 0
t.integer :poll_votes_count, null: false, default: 0
t.integer :poll_skips_count, null: false, default: 0
t.timestamps
end
end
end

View file

@ -0,0 +1,12 @@
class CreatePollOptions < ActiveRecord::Migration[5.2]
def change
create_table :poll_options do |t|
t.bigint :poll_id
t.string :markdown
t.string :processed_html
t.boolean :counts_in_tabulation
t.integer :poll_votes_count, null: false, default: 0
t.timestamps
end
end
end

View file

@ -0,0 +1,13 @@
class CreatePollVotes < ActiveRecord::Migration[5.2]
def change
create_table :poll_votes do |t|
t.bigint :user_id, null: false
t.bigint :poll_id, null: false
t.bigint :poll_option_id, null: false
t.timestamps
end
add_index :poll_votes, %i[poll_option_id user_id],
unique: true,
name: "index_poll_votes_on_poll_option_and_user"
end
end

View file

@ -0,0 +1,12 @@
class CreatePollSkips < ActiveRecord::Migration[5.2]
def change
create_table :poll_skips do |t|
t.bigint :user_id
t.bigint :poll_id
t.timestamps
end
add_index :poll_skips, %i[poll_id user_id],
unique: true,
name: "index_poll_skips_on_poll_and_user"
end
end

View file

@ -685,6 +685,45 @@ ActiveRecord::Schema.define(version: 2019_06_14_093041) do
t.index ["slug"], name: "index_podcasts_on_slug", unique: true
end
create_table "poll_options", force: :cascade do |t|
t.boolean "counts_in_tabulation"
t.datetime "created_at", null: false
t.string "markdown"
t.bigint "poll_id"
t.integer "poll_votes_count", default: 0, null: false
t.string "processed_html"
t.datetime "updated_at", null: false
end
create_table "poll_skips", force: :cascade do |t|
t.datetime "created_at", null: false
t.bigint "poll_id"
t.datetime "updated_at", null: false
t.bigint "user_id"
t.index ["poll_id", "user_id"], name: "index_poll_skips_on_poll_and_user", unique: true
end
create_table "poll_votes", force: :cascade do |t|
t.datetime "created_at", null: false
t.bigint "poll_id", null: false
t.bigint "poll_option_id", null: false
t.datetime "updated_at", null: false
t.bigint "user_id", null: false
t.index ["poll_option_id", "user_id"], name: "index_poll_votes_on_poll_option_and_user", unique: true
end
create_table "polls", force: :cascade do |t|
t.boolean "allow_multiple_selections", default: false
t.bigint "article_id"
t.datetime "created_at", null: false
t.integer "poll_options_count", default: 0, null: false
t.integer "poll_skips_count", default: 0, null: false
t.integer "poll_votes_count", default: 0, null: false
t.string "prompt_html"
t.string "prompt_markdown"
t.datetime "updated_at", null: false
end
create_table "push_notification_subscriptions", force: :cascade do |t|
t.string "auth_key"
t.datetime "created_at", null: false

View file

@ -0,0 +1,5 @@
FactoryBot.define do
factory :poll_option do
markdown { Faker::Hipster.words(3) }
end
end

View file

@ -0,0 +1,10 @@
FactoryBot.define do
factory :poll_skip do
# prompt_markdown { Faker::Hipster.words(5) }
# factory :poll_with_options do
# after(:create) do |poll|
# create_list(:poll_option, poll: poll)
# end
# end
end
end

View file

@ -0,0 +1,4 @@
FactoryBot.define do
factory :poll_vote do
end
end

6
spec/factories/polls.rb Normal file
View file

@ -0,0 +1,6 @@
FactoryBot.define do
factory :poll do
prompt_markdown { Faker::Hipster.words(5) }
poll_options_input_array { [rand(5).to_s, rand(5).to_s, rand(5).to_s, rand(5).to_s] }
end
end

View file

@ -413,6 +413,19 @@ RSpec.describe Article, type: :model do
article.main_image = "https://image.com/image.png"
expect(article.valid?).to eq(true)
end
it "does not allow the use of admin-only liquid tags for non-admins" do
poll = create(:poll, article_id: article.id)
article.body_markdown = "hello hey hey hey {% poll #{poll.id} %}"
expect(article.valid?).to eq(false)
end
it "allows admins" do
poll = create(:poll, article_id: article.id)
article.user.add_role(:admin)
article.body_markdown = "hello hey hey hey {% poll #{poll.id} %}"
expect(article.valid?).to eq(true)
end
end
it "updates main_image_background_hex_color" do

View file

@ -0,0 +1,15 @@
require "rails_helper"
RSpec.describe PollOption, type: :model do
let(:article) { create(:article, featured: true) }
let(:poll) { create(:poll, article_id: article.id) }
it "allows up to 128 markdown characters" do
poll_option = PollOption.create(markdown: "0" * 30, poll_id: poll.id)
expect(poll_option).to be_valid
end
it "disallows over 128 markdown characters" do
poll_option = PollOption.create(markdown: "0" * 200, poll_id: poll.id)
expect(poll_option).not_to be_valid
end
end

View file

@ -0,0 +1,35 @@
require 'rails_helper'
RSpec.describe PollSkip, type: :model do
let(:article) { create(:article, featured: true) }
let(:user) { create(:user) }
let(:poll) { create(:poll, article_id: article.id) }
it "is unique across poll and user" do
PollSkip.create(user_id: user.id, poll_id: poll.id)
PollSkip.create(user_id: user.id, poll_id: poll.id)
PollSkip.create(user_id: user.id, poll_id: poll.id)
expect(PollSkip.all.size).to eq(1)
second_poll = create(:poll, article_id: article.id)
PollSkip.create(user_id: user.id, poll_id: second_poll.id)
expect(PollSkip.all.size).to eq(2)
end
it "is unique across user and poll votes for the poll" do
PollVote.create(user_id: user.id, poll_id: poll.id, poll_option_id: poll.poll_options.last.id)
PollSkip.create(user_id: user.id, poll_id: poll.id)
PollSkip.create(user_id: user.id, poll_id: poll.id)
expect(PollSkip.all.size).to eq(0)
second_poll = create(:poll, article_id: article.id)
PollSkip.create(user_id: user.id, poll_id: second_poll.id)
expect(PollSkip.all.size).to eq(1)
end
it "is prevents a poll vote from being cast" do
PollSkip.create(user_id: user.id, poll_id: poll.id)
PollSkip.create(user_id: user.id, poll_id: poll.id)
PollVote.create(user_id: user.id, poll_id: poll.id, poll_option_id: poll.poll_options.last.id)
expect(PollSkip.all.size).to eq(1)
expect(PollVote.all.size).to eq(0)
end
end

17
spec/models/poll_spec.rb Normal file
View file

@ -0,0 +1,17 @@
require "rails_helper"
RSpec.describe Poll, type: :model do
let(:article) { create(:article, featured: true) }
let(:poll) { create(:poll, article_id: article.id) }
it "limits length of prompt" do
long_string = "0" * 200
poll.prompt_markdown = long_string
expect(poll).not_to be_valid
end
it "creates options from input" do
poll = create(:poll, article_id: article.id, poll_options_input_array: %w[hello goodbye heyheyhey])
expect(poll.poll_options.size).to eq(3)
end
end

View file

@ -0,0 +1,54 @@
require "rails_helper"
RSpec.describe PollVote, type: :model do
let(:article) { create(:article, featured: true) }
let(:user) { create(:user) }
let(:poll) { create(:poll, article_id: article.id) }
it "limits one vote per user per poll" do
create(:poll_vote, poll_option_id: poll.poll_options.last.id, user_id: user.id, poll_id: poll.id)
PollVote.create(poll_option_id: poll.poll_options.first.id, user_id: user.id, poll_id: poll.id)
PollVote.create(poll_option_id: poll.poll_options.last.id, user_id: user.id, poll_id: poll.id)
expect(user.poll_votes.size).to eq(1)
end
it "allows one vote per user across multiple polls" do
second_poll = create(:poll, article_id: article.id)
create(:poll_vote, poll_option_id: poll.poll_options.last.id, user_id: user.id, poll_id: poll.id)
create(:poll_vote, poll_option_id: second_poll.poll_options.last.id, user_id: user.id, poll_id: second_poll.id)
expect(user.reload.poll_votes.size).to eq(2)
end
it "allows multiple people to vote in one poll" do
second_user = create(:user)
create(:poll_vote, poll_option_id: poll.poll_options.last.id, user_id: user.id, poll_id: poll.id)
create(:poll_vote, poll_option_id: poll.poll_options.last.id, user_id: second_user.id, poll_id: poll.id)
expect(user.poll_votes.size).to eq(1)
expect(second_user.poll_votes.size).to eq(1)
end
it "disallows a user to skip poll after voting" do
create(:poll_vote, poll_option_id: poll.poll_options.last.id, user_id: user.id, poll_id: poll.id)
PollSkip.create(poll_id: poll.id, user_id: user.id)
expect(user.poll_skips.size).to eq(0)
end
it "disallows a vote after skipping" do
PollSkip.create(poll_id: poll.id, user_id: user.id)
PollVote.create(poll_option_id: poll.poll_options.last.id, user_id: user.id, poll_id: poll.id)
expect(user.poll_votes.size).to eq(0)
end
it "updates poll voting count after create" do
create(:poll_vote, poll_option_id: poll.poll_options.last.id, user_id: user.id, poll_id: poll.id)
expect(poll.reload.poll_votes_count).to eq(1)
expect(poll.reload.poll_options.last.poll_votes_count).to eq(1)
end
it "updates poll voting count after create with accuracy alongside skips" do
create(:poll_vote, poll_option_id: poll.poll_options.last.id, user_id: user.id, poll_id: poll.id)
PollSkip.create(poll_id: poll.id, user_id: user.id)
expect(poll.reload.poll_votes_count).to eq(1)
expect(poll.reload.poll_options.last.poll_votes_count).to eq(1)
end
end

View file

@ -0,0 +1,32 @@
require "rails_helper"
RSpec.describe "PollSkips", type: :request do
let(:user) { create(:user) }
let(:article) { create(:article) }
let(:poll) { create(:poll, article_id: article.id) }
before { sign_in user }
describe "POST /poll_votes" do
it "votes on behalf of current user" do
post "/poll_skips", params: {
poll_skip: { poll_id: poll.id }
}
expect(JSON.parse(response.body)["voting_data"]["votes_count"]).to eq(0)
expect(JSON.parse(response.body)["voting_data"]["votes_distribution"]).to include([poll.poll_options.first.id, 0])
expect(JSON.parse(response.body)["poll_id"]).to eq(poll.id)
expect(JSON.parse(response.body)["voted"]).to eq(false)
expect(user.poll_skips.size).to eq(1)
end
it "only allows one of vote or skip" do
post "/poll_skips", params: {
poll_skip: { poll_id: poll.id }
}
post "/poll_votes", params: {
poll_vote: { poll_option_id: poll.poll_options.first.id }
}
expect(user.poll_skips.size).to eq(1)
expect(user.poll_votes.size).to eq(0)
end
end
end

View file

@ -0,0 +1,53 @@
require "rails_helper"
RSpec.describe "PollVotes", type: :request do
let(:user) { create(:user) }
let(:article) { create(:article) }
let(:poll) { create(:poll, article_id: article.id) }
before { sign_in user }
describe "GET /poll_votes" do
it "returns proper results for poll" do
get "/poll_votes/#{poll.id}"
expect(JSON.parse(response.body)["voting_data"]["votes_count"]).to eq(0)
expect(JSON.parse(response.body)["voting_data"]["votes_distribution"]).to include([poll.poll_options.first.id, 0])
expect(JSON.parse(response.body)["poll_id"]).to eq(poll.id)
expect(JSON.parse(response.body)["voted"]).to eq(false)
end
it "returns proper results for poll if voted" do
create(:poll_vote, user_id: user.id, poll_option_id: poll.poll_options.first.id, poll_id: poll.id)
get "/poll_votes/#{poll.id}"
expect(JSON.parse(response.body)["voting_data"]["votes_count"]).to eq(1)
expect(JSON.parse(response.body)["voting_data"]["votes_distribution"]).to include([poll.poll_options.first.id, 1])
expect(JSON.parse(response.body)["poll_id"]).to eq(poll.id)
expect(JSON.parse(response.body)["voted"]).to eq(true)
end
end
describe "POST /poll_votes" do
it "votes on behalf of current user" do
post "/poll_votes", params: {
poll_vote: { poll_option_id: poll.poll_options.first.id }
}
expect(JSON.parse(response.body)["voting_data"]["votes_count"]).to eq(1)
expect(JSON.parse(response.body)["voting_data"]["votes_distribution"]).to include([poll.poll_options.first.id, 1])
expect(JSON.parse(response.body)["poll_id"]).to eq(poll.id)
expect(JSON.parse(response.body)["voted"]).to eq(true)
expect(user.poll_votes.size).to eq(1)
end
it "votes on behalf of current user only once" do
post "/poll_votes", params: {
poll_vote: { poll_option_id: poll.poll_options.first.id }
}
post "/poll_votes", params: {
poll_vote: { poll_option_id: poll.poll_options.first.id }
}
expect(JSON.parse(response.body)["voting_data"]["votes_count"]).to eq(1)
expect(JSON.parse(response.body)["voting_data"]["votes_distribution"]).to include([poll.poll_options.first.id, 1])
expect(JSON.parse(response.body)["voted"]).to eq(true)
expect(user.poll_votes.size).to eq(1)
end
end
end