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:
parent
f29b67a7b6
commit
8ae057b06e
32 changed files with 722 additions and 4 deletions
|
|
@ -10,3 +10,4 @@
|
|||
@import 'InstagramTag';
|
||||
@import 'GistTag';
|
||||
@import 'GithubReadmeTag';
|
||||
@import 'PollTag';
|
||||
|
|
|
|||
85
app/assets/stylesheets/ltags/PollTag.scss
Normal file
85
app/assets/stylesheets/ltags/PollTag.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
app/controllers/poll_skips_controller.rb
Normal file
19
app/controllers/poll_skips_controller.rb
Normal 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
|
||||
30
app/controllers/poll_votes_controller.rb
Normal file
30
app/controllers/poll_votes_controller.rb
Normal 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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
121
app/liquid_tags/poll_tag.rb
Normal 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)
|
||||
|
|
@ -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
35
app/models/poll.rb
Normal 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
17
app/models/poll_option.rb
Normal 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
12
app/models/poll_skip.rb
Normal 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
30
app/models/poll_vote.rb
Normal 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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -236,6 +236,7 @@
|
|||
<%= PodcastTag.script.html_safe %>
|
||||
<%= GistTag.script.html_safe %>
|
||||
<%= RunkitTag.script.html_safe %>
|
||||
<%= PollTag.script.html_safe %>
|
||||
</script>
|
||||
<% end %>
|
||||
|
||||
|
|
|
|||
12
app/views/liquids/_poll.html.erb
Normal file
12
app/views/liquids/_poll.html.erb
Normal 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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
14
db/migrate/20190525233909_create_polls.rb
Normal file
14
db/migrate/20190525233909_create_polls.rb
Normal 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
|
||||
12
db/migrate/20190525233918_create_poll_options.rb
Normal file
12
db/migrate/20190525233918_create_poll_options.rb
Normal 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
|
||||
13
db/migrate/20190525233934_create_poll_votes.rb
Normal file
13
db/migrate/20190525233934_create_poll_votes.rb
Normal 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
|
||||
12
db/migrate/20190611195955_create_poll_skips.rb
Normal file
12
db/migrate/20190611195955_create_poll_skips.rb
Normal 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
|
||||
39
db/schema.rb
39
db/schema.rb
|
|
@ -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
|
||||
|
|
|
|||
5
spec/factories/poll_options.rb
Normal file
5
spec/factories/poll_options.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
FactoryBot.define do
|
||||
factory :poll_option do
|
||||
markdown { Faker::Hipster.words(3) }
|
||||
end
|
||||
end
|
||||
10
spec/factories/poll_skips.rb
Normal file
10
spec/factories/poll_skips.rb
Normal 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
|
||||
4
spec/factories/poll_votes.rb
Normal file
4
spec/factories/poll_votes.rb
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
FactoryBot.define do
|
||||
factory :poll_vote do
|
||||
end
|
||||
end
|
||||
6
spec/factories/polls.rb
Normal file
6
spec/factories/polls.rb
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
15
spec/models/poll_option_spec.rb
Normal file
15
spec/models/poll_option_spec.rb
Normal 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
|
||||
35
spec/models/poll_skip_spec.rb
Normal file
35
spec/models/poll_skip_spec.rb
Normal 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
17
spec/models/poll_spec.rb
Normal 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
|
||||
54
spec/models/poll_vote_spec.rb
Normal file
54
spec/models/poll_vote_spec.rb
Normal 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
|
||||
32
spec/requests/poll_skips_spec.rb
Normal file
32
spec/requests/poll_skips_spec.rb
Normal 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
|
||||
53
spec/requests/poll_votes_spec.rb
Normal file
53
spec/requests/poll_votes_spec.rb
Normal 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
|
||||
Loading…
Add table
Reference in a new issue