Merge pull request #1258 from consul/polling-questions
Polling questions
This commit is contained in:
17
app/controllers/polls/questions_controller.rb
Normal file
17
app/controllers/polls/questions_controller.rb
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
class Polls::QuestionsController < ApplicationController
|
||||||
|
|
||||||
|
load_and_authorize_resource :poll
|
||||||
|
load_and_authorize_resource :question, class: 'Poll::Question', through: :poll
|
||||||
|
|
||||||
|
def answer
|
||||||
|
partial_result = @question.partial_results.find_or_initialize_by(author: current_user,
|
||||||
|
amount: 1,
|
||||||
|
origin: 'web')
|
||||||
|
|
||||||
|
partial_result.answer = params[:answer]
|
||||||
|
partial_result.save!
|
||||||
|
|
||||||
|
@answers_by_question_id = {@question.id => params[:answer]}
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
22
app/controllers/polls_controller.rb
Normal file
22
app/controllers/polls_controller.rb
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
class PollsController < ApplicationController
|
||||||
|
|
||||||
|
load_and_authorize_resource
|
||||||
|
|
||||||
|
has_filters %w{current expired incoming}
|
||||||
|
|
||||||
|
def index
|
||||||
|
@polls = @polls.send(@current_filter).sort_for_list.page(params[:page])
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
@answerable_questions = @poll.questions.answerable_by(current_user).for_render.sort_for_list
|
||||||
|
@non_answerable_questions = @poll.questions.where.not(id: @answerable_questions.map(&:id)).for_render.sort_for_list
|
||||||
|
|
||||||
|
@answers_by_question_id = {}
|
||||||
|
poll_partial_results = Poll::PartialResult.by_question(@poll.question_ids).by_author(current_user.try(:id))
|
||||||
|
poll_partial_results.each do |result|
|
||||||
|
@answers_by_question_id[result.question_id] = result.answer
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -48,6 +48,10 @@ module Abilities
|
|||||||
can :create, SpendingProposal
|
can :create, SpendingProposal
|
||||||
can :create, DirectMessage
|
can :create, DirectMessage
|
||||||
can :show, DirectMessage, sender_id: user.id
|
can :show, DirectMessage, sender_id: user.id
|
||||||
|
can(:answer, Poll, Poll.answerable_by(user)){ |poll| poll.answerable_by?(user) }
|
||||||
|
can(:answer, Poll::Question, Poll::Question.answerable_by(user)) do |question|
|
||||||
|
question.answerable_by?(user)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
can [:create, :show], ProposalNotification, proposal: { author_id: user.id }
|
can [:create, :show], ProposalNotification, proposal: { author_id: user.id }
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ module Abilities
|
|||||||
can [:read, :map], Debate
|
can [:read, :map], Debate
|
||||||
can [:read, :map, :summary], Proposal
|
can [:read, :map, :summary], Proposal
|
||||||
can :read, Comment
|
can :read, Comment
|
||||||
|
can :read, Poll
|
||||||
can :read, SpendingProposal
|
can :read, SpendingProposal
|
||||||
can :read, Legislation
|
can :read, Legislation
|
||||||
can :read, User
|
can :read, User
|
||||||
|
|||||||
@@ -1,6 +1,34 @@
|
|||||||
class Poll < ActiveRecord::Base
|
class Poll < ActiveRecord::Base
|
||||||
has_many :booths
|
has_many :booths
|
||||||
has_many :voters, through: :booths, class_name: "Poll::Voter"
|
has_many :voters, through: :booths, class_name: "Poll::Voter"
|
||||||
|
has_many :questions
|
||||||
|
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
end
|
|
||||||
|
scope :current, -> { where('starts_at <= ? and ? <= ends_at', Time.now, Time.now) }
|
||||||
|
scope :incoming, -> { where('? < starts_at', Time.now) }
|
||||||
|
scope :expired, -> { where('ends_at < ?', Time.now) }
|
||||||
|
|
||||||
|
scope :sort_for_list, -> { order(:starts_at) }
|
||||||
|
|
||||||
|
def current?(timestamp = DateTime.now)
|
||||||
|
starts_at <= timestamp && timestamp <= ends_at
|
||||||
|
end
|
||||||
|
|
||||||
|
def incoming?(timestamp = DateTime.now)
|
||||||
|
timestamp < starts_at
|
||||||
|
end
|
||||||
|
|
||||||
|
def expired?(timestamp = DateTime.now)
|
||||||
|
ends_at < timestamp
|
||||||
|
end
|
||||||
|
|
||||||
|
def answerable_by?(user)
|
||||||
|
user.present? && user.level_two_or_three_verified? && current?
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.answerable_by(user)
|
||||||
|
return none if user.nil? || user.unverified?
|
||||||
|
current
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|||||||
18
app/models/poll/partial_result.rb
Normal file
18
app/models/poll/partial_result.rb
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
class Poll::PartialResult < ActiveRecord::Base
|
||||||
|
|
||||||
|
VALID_ORIGINS = %w{ web }
|
||||||
|
|
||||||
|
belongs_to :question, -> { with_hidden }
|
||||||
|
belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id'
|
||||||
|
|
||||||
|
validates :question, presence: true
|
||||||
|
validates :author, presence: true
|
||||||
|
validates :answer, presence: true
|
||||||
|
validates :answer, inclusion: {in: ->(a) { a.question.valid_answers }}
|
||||||
|
validates :origin, inclusion: {in: VALID_ORIGINS}
|
||||||
|
|
||||||
|
scope :by_author, -> (author_id) { where(author_id: author_id) }
|
||||||
|
scope :by_question, -> (question_id) { where(question_id: question_id) }
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
66
app/models/poll/question.rb
Normal file
66
app/models/poll/question.rb
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
class Poll::Question < ActiveRecord::Base
|
||||||
|
include Measurable
|
||||||
|
|
||||||
|
acts_as_paranoid column: :hidden_at
|
||||||
|
include ActsAsParanoidAliases
|
||||||
|
|
||||||
|
belongs_to :poll
|
||||||
|
belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id'
|
||||||
|
|
||||||
|
has_many :comments, as: :commentable
|
||||||
|
has_many :answers
|
||||||
|
has_many :partial_results
|
||||||
|
has_and_belongs_to_many :geozones
|
||||||
|
belongs_to :proposal
|
||||||
|
|
||||||
|
validates :title, presence: true
|
||||||
|
validates :question, presence: true
|
||||||
|
validates :summary, presence: true
|
||||||
|
validates :author, presence: true
|
||||||
|
|
||||||
|
validates :title, length: { in: 4..Poll::Question.title_max_length }
|
||||||
|
validates :description, length: { maximum: Poll::Question.description_max_length }
|
||||||
|
validates :question, length: { in: 10..Poll::Question.question_max_length }
|
||||||
|
|
||||||
|
scope :sort_for_list, -> { order('poll_questions.proposal_id IS NULL', :created_at)}
|
||||||
|
scope :for_render, -> { includes(:author, :proposal) }
|
||||||
|
scope :by_geozone, -> (geozone_id) { joins(:geozones).where(geozones: {id: geozone_id}) }
|
||||||
|
|
||||||
|
def description
|
||||||
|
super.try :html_safe
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_answers
|
||||||
|
(super.try(:split, ',').compact || []).map(&:strip)
|
||||||
|
end
|
||||||
|
|
||||||
|
def copy_attributes_from_proposal(proposal)
|
||||||
|
if proposal.present?
|
||||||
|
self.author = proposal.author
|
||||||
|
self.author_visible_name = proposal.author.name
|
||||||
|
self.proposal_id = proposal.id
|
||||||
|
self.title = proposal.title
|
||||||
|
self.description = proposal.description
|
||||||
|
self.summary = proposal.summary
|
||||||
|
self.question = proposal.question
|
||||||
|
self.all_geozones = true
|
||||||
|
self.valid_answers = I18n.t('poll_questions.default_valid_answers')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def answerable_by?(user)
|
||||||
|
poll.answerable_by?(user) && (self.all_geozones || self.geozone_ids.include?(user.geozone_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.answerable_by(user)
|
||||||
|
return none if user.nil? || user.unverified?
|
||||||
|
|
||||||
|
joins('LEFT JOIN "geozones_poll_questions" ON "geozones_poll_questions"."question_id" = "poll_questions"."id"')
|
||||||
|
.where('poll_questions.poll_id IN (?) AND (poll_questions.all_geozones = ? OR geozones_poll_questions.geozone_id = ?)',
|
||||||
|
Poll.answerable_by(user).pluck(:id),
|
||||||
|
true,
|
||||||
|
user.geozone_id || -1) # user.geozone_id can be nil, which would throw errors on sql
|
||||||
|
.group('poll_questions.id')
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
7
app/views/polls/index.html.erb
Normal file
7
app/views/polls/index.html.erb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<%= render 'shared/filter_subnav', i18n_namespace: "polls.index" %>
|
||||||
|
|
||||||
|
<% @polls.each do |poll| %>
|
||||||
|
<%= link_to poll.name, poll %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= paginate @polls %>
|
||||||
23
app/views/polls/questions/_answers.html.erb
Normal file
23
app/views/polls/questions/_answers.html.erb
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<div class="enquiries-answers">
|
||||||
|
<% if can? :answer, question %>
|
||||||
|
<div class="small-12 small-centered text-center column">
|
||||||
|
<% question.valid_answers.each do |answer| %>
|
||||||
|
<% if @answers_by_question_id[question.id] == answer %>
|
||||||
|
<span class="button answered-fixme-decabeza">
|
||||||
|
<%= answer %>
|
||||||
|
</span>
|
||||||
|
<% else %>
|
||||||
|
<%= link_to answer,
|
||||||
|
answer_poll_question_path(poll_id: question.poll_id, id: question.id, answer: answer),
|
||||||
|
method: :post,
|
||||||
|
remote: true,
|
||||||
|
class: "button secondary hollow" %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<% question.valid_answers.each do |answer| %>
|
||||||
|
<span class="button deactivated-fixme-decabeza"><%= answer %></span>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
1
app/views/polls/questions/answer.js.erb
Normal file
1
app/views/polls/questions/answer.js.erb
Normal file
@@ -0,0 +1 @@
|
|||||||
|
$("#<%= dom_id(@question) %>_answers").html('<%= j render("polls/questions/answers", question: @question) %>');
|
||||||
53
app/views/polls/show.html.erb
Normal file
53
app/views/polls/show.html.erb
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<%= @poll.name %>
|
||||||
|
|
||||||
|
<% unless can?(:answer, @poll) %>
|
||||||
|
<div class="small-12 column">
|
||||||
|
<% if current_user.nil? %>
|
||||||
|
<div class="callout primary">
|
||||||
|
<%= t("polls.show.cant_answer_not_logged_in",
|
||||||
|
signin: link_to(t("polls.show.signin"), new_user_session_path, class: "probe-message"),
|
||||||
|
signup: link_to(t("polls.show.signup"), new_user_registration_path, class: "probe-message")).html_safe %>
|
||||||
|
</div>
|
||||||
|
<% elsif current_user.unverified? %>
|
||||||
|
<div class="callout warning">
|
||||||
|
<%= t('polls.show.cant_answer_verify_html',
|
||||||
|
verify_link: link_to(t('polls.show.verify_link'), verification_path)) %>
|
||||||
|
</div>
|
||||||
|
<% elsif @poll.incoming? %>
|
||||||
|
<div class="callout primary">
|
||||||
|
<%= t('polls.show.cant_answer_incoming') %>
|
||||||
|
</div>
|
||||||
|
<% elsif @poll.expired? %>
|
||||||
|
<div class="callout alert">
|
||||||
|
<%= t('polls.show.cant_answer_expired') %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% @answerable_questions.each do |question| %>
|
||||||
|
<div id="<%= dom_id(question) %>">
|
||||||
|
<%= question.title %>
|
||||||
|
|
||||||
|
<div class="row margin-top text-center" id="<%= dom_id(question) %>_answers">
|
||||||
|
<%= render 'polls/questions/answers', question: question %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if can?(:answer, @poll) &&
|
||||||
|
@non_answerable_questions.present? %>
|
||||||
|
<div class="callout warning">
|
||||||
|
<%= t('polls.show.cant_answer_wrong_geozone') %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% @non_answerable_questions.each do |question| %>
|
||||||
|
<div id="<%= dom_id(question) %>">
|
||||||
|
<%= question.title %>
|
||||||
|
|
||||||
|
<div class="row margin-top text-center" id="<%= dom_id(question) %>_answers">
|
||||||
|
<%= render 'polls/questions/answers', question: question %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
@@ -375,6 +375,18 @@ en:
|
|||||||
update:
|
update:
|
||||||
form:
|
form:
|
||||||
submit_button: Save changes
|
submit_button: Save changes
|
||||||
|
polls:
|
||||||
|
show:
|
||||||
|
cant_answer_not_logged_in: "You must %{signin} or %{signup} to participate."
|
||||||
|
signin: Sign in
|
||||||
|
signup: Sign up
|
||||||
|
cant_answer_verify_html: "You must %{verify_link} in order to answer."
|
||||||
|
verify_link: "verify your account"
|
||||||
|
cant_answer_incoming: "This poll has not yet started."
|
||||||
|
cant_answer_expired: "This poll has finished."
|
||||||
|
cant_answer_wrong_geozone: "The following questions are not available in your geozone."
|
||||||
|
poll_questions:
|
||||||
|
default_valid_answers: "Yes, No"
|
||||||
proposal_ballots:
|
proposal_ballots:
|
||||||
title: "Votings"
|
title: "Votings"
|
||||||
description_html: "The following citizen proposals that have reached the <strong>required supports</strong> and will be voted."
|
description_html: "The following citizen proposals that have reached the <strong>required supports</strong> and will be voted."
|
||||||
|
|||||||
@@ -85,6 +85,12 @@ Rails.application.routes.draw do
|
|||||||
get :search, on: :collection
|
get :search, on: :collection
|
||||||
end
|
end
|
||||||
|
|
||||||
|
resources :polls, only: [:show, :index] do
|
||||||
|
resources :questions, only: [], controller: 'polls/questions' do
|
||||||
|
post :answer, on: :member
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
resources :users, only: [:show] do
|
resources :users, only: [:show] do
|
||||||
resources :direct_messages, only: [:new, :create, :show]
|
resources :direct_messages, only: [:new, :create, :show]
|
||||||
end
|
end
|
||||||
|
|||||||
21
db/migrate/20161028104156_create_poll_questions.rb
Normal file
21
db/migrate/20161028104156_create_poll_questions.rb
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
class CreatePollQuestions < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :poll_questions do |t|
|
||||||
|
t.references :proposal, index: true, foreign_key: true
|
||||||
|
t.references :poll, index: true, foreign_key: true
|
||||||
|
t.references :author, index: true # foreign key added later due to rails 4
|
||||||
|
t.string :author_visible_name
|
||||||
|
t.string :title
|
||||||
|
t.string :question
|
||||||
|
t.string :summary
|
||||||
|
t.string :valid_answers
|
||||||
|
t.text :description
|
||||||
|
t.integer :comments_count
|
||||||
|
t.datetime :hidden_at
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_foreign_key :poll_questions, :users, column: :author_id
|
||||||
|
end
|
||||||
|
end
|
||||||
10
db/migrate/20161028143204_create_geozones_poll_questions.rb
Normal file
10
db/migrate/20161028143204_create_geozones_poll_questions.rb
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
class CreateGeozonesPollQuestions < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :geozones_poll_questions do |t|
|
||||||
|
t.references :geozone, index: true, foreign_key: true
|
||||||
|
t.integer :question_id, index: true
|
||||||
|
end
|
||||||
|
|
||||||
|
add_foreign_key :geozones_poll_questions, :poll_questions, column: :question_id
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
class AddAllGeozonesToPollQuestions < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
add_column :poll_questions, :all_geozones, :boolean, default: false
|
||||||
|
end
|
||||||
|
end
|
||||||
14
db/migrate/20161107174423_create_poll_partial_result.rb
Normal file
14
db/migrate/20161107174423_create_poll_partial_result.rb
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
class CreatePollPartialResult < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :poll_partial_results do |t|
|
||||||
|
t.integer :question_id, index: true
|
||||||
|
t.integer :author_id, index: true
|
||||||
|
t.string :answer, index: true
|
||||||
|
t.integer :amount
|
||||||
|
t.string :origin, index: true
|
||||||
|
end
|
||||||
|
|
||||||
|
add_foreign_key(:poll_partial_results, :users, column: :author_id)
|
||||||
|
add_foreign_key(:poll_partial_results, :poll_questions, column: :question_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
51
db/schema.rb
51
db/schema.rb
@@ -11,7 +11,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 20161102133838) do
|
ActiveRecord::Schema.define(version: 20161107174423) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@@ -211,6 +211,14 @@ ActiveRecord::Schema.define(version: 20161102133838) do
|
|||||||
t.string "census_code"
|
t.string "census_code"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "geozones_poll_questions", force: :cascade do |t|
|
||||||
|
t.integer "geozone_id"
|
||||||
|
t.integer "question_id"
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index "geozones_poll_questions", ["geozone_id"], name: "index_geozones_poll_questions_on_geozone_id", using: :btree
|
||||||
|
add_index "geozones_poll_questions", ["question_id"], name: "index_geozones_poll_questions_on_question_id", using: :btree
|
||||||
|
|
||||||
create_table "identities", force: :cascade do |t|
|
create_table "identities", force: :cascade do |t|
|
||||||
t.integer "user_id"
|
t.integer "user_id"
|
||||||
t.string "provider"
|
t.string "provider"
|
||||||
@@ -287,6 +295,40 @@ ActiveRecord::Schema.define(version: 20161102133838) do
|
|||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "poll_partial_results", force: :cascade do |t|
|
||||||
|
t.integer "question_id"
|
||||||
|
t.integer "author_id"
|
||||||
|
t.string "answer"
|
||||||
|
t.integer "amount"
|
||||||
|
t.string "origin"
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index "poll_partial_results", ["answer"], name: "index_poll_partial_results_on_answer", using: :btree
|
||||||
|
add_index "poll_partial_results", ["author_id"], name: "index_poll_partial_results_on_author_id", using: :btree
|
||||||
|
add_index "poll_partial_results", ["origin"], name: "index_poll_partial_results_on_origin", using: :btree
|
||||||
|
add_index "poll_partial_results", ["question_id"], name: "index_poll_partial_results_on_question_id", using: :btree
|
||||||
|
|
||||||
|
create_table "poll_questions", force: :cascade do |t|
|
||||||
|
t.integer "proposal_id"
|
||||||
|
t.integer "poll_id"
|
||||||
|
t.integer "author_id"
|
||||||
|
t.string "author_visible_name"
|
||||||
|
t.string "title"
|
||||||
|
t.string "question"
|
||||||
|
t.string "summary"
|
||||||
|
t.string "valid_answers"
|
||||||
|
t.text "description"
|
||||||
|
t.integer "comments_count"
|
||||||
|
t.datetime "hidden_at"
|
||||||
|
t.datetime "created_at"
|
||||||
|
t.datetime "updated_at"
|
||||||
|
t.boolean "all_geozones", default: false
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index "poll_questions", ["author_id"], name: "index_poll_questions_on_author_id", using: :btree
|
||||||
|
add_index "poll_questions", ["poll_id"], name: "index_poll_questions_on_poll_id", using: :btree
|
||||||
|
add_index "poll_questions", ["proposal_id"], name: "index_poll_questions_on_proposal_id", using: :btree
|
||||||
|
|
||||||
create_table "poll_voters", force: :cascade do |t|
|
create_table "poll_voters", force: :cascade do |t|
|
||||||
t.integer "booth_id"
|
t.integer "booth_id"
|
||||||
t.string "document_number"
|
t.string "document_number"
|
||||||
@@ -583,12 +625,19 @@ ActiveRecord::Schema.define(version: 20161102133838) do
|
|||||||
add_foreign_key "annotations", "users"
|
add_foreign_key "annotations", "users"
|
||||||
add_foreign_key "failed_census_calls", "users"
|
add_foreign_key "failed_census_calls", "users"
|
||||||
add_foreign_key "flags", "users"
|
add_foreign_key "flags", "users"
|
||||||
|
add_foreign_key "geozones_poll_questions", "geozones"
|
||||||
|
add_foreign_key "geozones_poll_questions", "poll_questions", column: "question_id"
|
||||||
add_foreign_key "identities", "users"
|
add_foreign_key "identities", "users"
|
||||||
add_foreign_key "locks", "users"
|
add_foreign_key "locks", "users"
|
||||||
add_foreign_key "managers", "users"
|
add_foreign_key "managers", "users"
|
||||||
add_foreign_key "moderators", "users"
|
add_foreign_key "moderators", "users"
|
||||||
add_foreign_key "notifications", "users"
|
add_foreign_key "notifications", "users"
|
||||||
add_foreign_key "organizations", "users"
|
add_foreign_key "organizations", "users"
|
||||||
|
add_foreign_key "poll_partial_results", "poll_questions", column: "question_id"
|
||||||
|
add_foreign_key "poll_partial_results", "users", column: "author_id"
|
||||||
|
add_foreign_key "poll_questions", "polls"
|
||||||
|
add_foreign_key "poll_questions", "proposals"
|
||||||
|
add_foreign_key "poll_questions", "users", column: "author_id"
|
||||||
add_foreign_key "users", "geozones"
|
add_foreign_key "users", "geozones"
|
||||||
add_foreign_key "valuators", "users"
|
add_foreign_key "valuators", "users"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -265,10 +265,21 @@ FactoryGirl.define do
|
|||||||
|
|
||||||
factory :poll do
|
factory :poll do
|
||||||
sequence(:name) { |n| "Poll #{n}" }
|
sequence(:name) { |n| "Poll #{n}" }
|
||||||
|
|
||||||
|
starts_at { 1.month.ago }
|
||||||
|
ends_at { 1.month.from_now }
|
||||||
|
|
||||||
|
trait :incoming do
|
||||||
|
starts_at { 2.days.from_now }
|
||||||
|
ends_at { 1.month.from_now }
|
||||||
|
end
|
||||||
|
|
||||||
|
trait :expired do
|
||||||
|
starts_at { 1.month.ago }
|
||||||
|
ends_at { 15.days.ago }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
factory :poll_officer, class: 'Poll::Officer' do
|
factory :poll_officer, class: 'Poll::Officer' do
|
||||||
user
|
user
|
||||||
end
|
end
|
||||||
@@ -278,7 +289,6 @@ FactoryGirl.define do
|
|||||||
association :booth, factory: :poll_booth
|
association :booth, factory: :poll_booth
|
||||||
end
|
end
|
||||||
|
|
||||||
>>>>>>> assigns officers to booths
|
|
||||||
factory :poll_booth, class: 'Poll::Booth' do
|
factory :poll_booth, class: 'Poll::Booth' do
|
||||||
sequence(:name) { |n| "Booth #{n}" }
|
sequence(:name) { |n| "Booth #{n}" }
|
||||||
sequence(:location) { |n| "Street #{n}" }
|
sequence(:location) { |n| "Street #{n}" }
|
||||||
@@ -299,7 +309,23 @@ FactoryGirl.define do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
>>>>>>> validates voter in census
|
factory :poll_question, class: 'Poll::Question' do
|
||||||
|
poll
|
||||||
|
association :author, factory: :user
|
||||||
|
sequence(:title) { |n| "Question title #{n}" }
|
||||||
|
sequence(:summary) { |n| "Question summary #{n}" }
|
||||||
|
sequence(:description) { |n| "Question description #{n}" }
|
||||||
|
sequence(:question) { |n| "Question question #{n}" }
|
||||||
|
valid_answers { Faker::Lorem.words(3).join(', ') }
|
||||||
|
end
|
||||||
|
|
||||||
|
factory :poll_partial_result, class: 'Poll::PartialResult' do
|
||||||
|
association :question, factory: :poll_question
|
||||||
|
association :author, factory: :user
|
||||||
|
origin { 'web' }
|
||||||
|
answer { question.verified_answers.sample }
|
||||||
|
end
|
||||||
|
|
||||||
factory :organization do
|
factory :organization do
|
||||||
user
|
user
|
||||||
responsible_name "Johnny Utah"
|
responsible_name "Johnny Utah"
|
||||||
|
|||||||
187
spec/features/polls_spec.rb
Normal file
187
spec/features/polls_spec.rb
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
feature 'Polls' do
|
||||||
|
|
||||||
|
context '#index' do
|
||||||
|
|
||||||
|
scenario 'Polls can be listed' do
|
||||||
|
polls = create_list(:poll, 3)
|
||||||
|
|
||||||
|
visit polls_path
|
||||||
|
|
||||||
|
polls.each do |poll|
|
||||||
|
expect(page).to have_link(poll.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Filtering polls' do
|
||||||
|
create(:poll, name: "Current poll")
|
||||||
|
create(:poll, :incoming, name: "Incoming poll")
|
||||||
|
create(:poll, :expired, name: "Expired poll")
|
||||||
|
|
||||||
|
visit polls_path
|
||||||
|
expect(page).to have_content('Current poll')
|
||||||
|
expect(page).to_not have_content('Incoming poll')
|
||||||
|
expect(page).to_not have_content('Expired poll')
|
||||||
|
|
||||||
|
visit polls_path(filter: 'incoming')
|
||||||
|
expect(page).to_not have_content('Current poll')
|
||||||
|
expect(page).to have_content('Incoming poll')
|
||||||
|
expect(page).to_not have_content('Expired poll')
|
||||||
|
|
||||||
|
visit polls_path(filter: 'expired')
|
||||||
|
expect(page).to_not have_content('Current poll')
|
||||||
|
expect(page).to_not have_content('Incoming poll')
|
||||||
|
expect(page).to have_content('Expired poll')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario "Current filter is properly highlighted" do
|
||||||
|
visit polls_path
|
||||||
|
expect(page).to_not have_link('Current')
|
||||||
|
expect(page).to have_link('Incoming')
|
||||||
|
expect(page).to have_link('Expired')
|
||||||
|
|
||||||
|
visit polls_path(filter: 'incoming')
|
||||||
|
expect(page).to have_link('Current')
|
||||||
|
expect(page).to_not have_link('Incoming')
|
||||||
|
expect(page).to have_link('Expired')
|
||||||
|
|
||||||
|
visit polls_path(filter: 'expired')
|
||||||
|
expect(page).to have_link('Current')
|
||||||
|
expect(page).to have_link('Incoming')
|
||||||
|
expect(page).to_not have_link('Expired')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'Show' do
|
||||||
|
let(:geozone) { create(:geozone) }
|
||||||
|
let(:poll) { create(:poll) }
|
||||||
|
|
||||||
|
scenario 'Lists questions from proposals as well as regular ones' do
|
||||||
|
normal_question = create(:poll_question, poll: poll)
|
||||||
|
proposal_question = create(:poll_question, poll: poll, proposal: create(:proposal))
|
||||||
|
|
||||||
|
visit poll_path(poll)
|
||||||
|
expect(page).to have_content(poll.name)
|
||||||
|
|
||||||
|
expect(page).to have_content(normal_question.title)
|
||||||
|
expect(page).to have_content(proposal_question.title)
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Non-logged in users' do
|
||||||
|
create(:poll_question, poll: poll, valid_answers: 'Han Solo, Chewbacca')
|
||||||
|
visit poll_path(poll)
|
||||||
|
|
||||||
|
expect(page).to have_content('Han Solo')
|
||||||
|
expect(page).to have_content('Chewbacca')
|
||||||
|
expect(page).to have_content('You must Sign in or Sign up to participate')
|
||||||
|
|
||||||
|
expect(page).to_not have_link('Han Solo')
|
||||||
|
expect(page).to_not have_link('Chewbacca')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Level 1 users' do
|
||||||
|
create(:poll_question, poll: poll, geozone_ids: [geozone.id], valid_answers: 'Han Solo, Chewbacca')
|
||||||
|
login_as(create(:user, geozone: geozone))
|
||||||
|
visit poll_path(poll)
|
||||||
|
|
||||||
|
expect(page).to have_content('You must verify your account in order to answer')
|
||||||
|
|
||||||
|
expect(page).to have_content('Han Solo')
|
||||||
|
expect(page).to have_content('Chewbacca')
|
||||||
|
|
||||||
|
expect(page).to_not have_link('Han Solo')
|
||||||
|
expect(page).to_not have_link('Chewbacca')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Level 2 users in an incoming question' do
|
||||||
|
incoming_poll = create(:poll, :incoming)
|
||||||
|
create(:poll_question, poll: incoming_poll, geozone_ids: [geozone.id], valid_answers: 'Rey, Finn')
|
||||||
|
login_as(create(:user, :level_two, geozone: geozone))
|
||||||
|
|
||||||
|
visit poll_path(incoming_poll)
|
||||||
|
|
||||||
|
expect(page).to have_content('Rey')
|
||||||
|
expect(page).to have_content('Finn')
|
||||||
|
expect(page).to_not have_link('Rey')
|
||||||
|
expect(page).to_not have_link('Finn')
|
||||||
|
|
||||||
|
expect(page).to have_content('This poll has not yet started')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Level 2 users in an expired question' do
|
||||||
|
expired_poll = create(:poll, :expired)
|
||||||
|
create(:poll_question, poll: expired_poll, geozone_ids: [geozone.id], valid_answers: 'Luke, Leia')
|
||||||
|
login_as(create(:user, :level_two, geozone: geozone))
|
||||||
|
|
||||||
|
visit poll_path(expired_poll)
|
||||||
|
|
||||||
|
expect(page).to have_content('Luke')
|
||||||
|
expect(page).to have_content('Leia')
|
||||||
|
expect(page).to_not have_link('Luke')
|
||||||
|
expect(page).to_not have_link('Leia')
|
||||||
|
|
||||||
|
expect(page).to have_content('This poll has finished')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Level 2 users in a poll with questions for a geozone which is not theirs' do
|
||||||
|
create(:poll_question, poll: poll, geozone_ids: [], valid_answers: 'Vader, Palpatine')
|
||||||
|
login_as(create(:user, :level_two))
|
||||||
|
|
||||||
|
visit poll_path(poll)
|
||||||
|
|
||||||
|
expect(page).to have_content('The following questions are not available in your geozone')
|
||||||
|
|
||||||
|
expect(page).to have_content('Vader')
|
||||||
|
expect(page).to have_content('Palpatine')
|
||||||
|
expect(page).to_not have_link('Vader')
|
||||||
|
expect(page).to_not have_link('Palpatine')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Level 2 users reading a same-geozone question' do
|
||||||
|
create(:poll_question, poll: poll, geozone_ids: [geozone.id], valid_answers: 'Han Solo, Chewbacca')
|
||||||
|
login_as(create(:user, :level_two, geozone: geozone))
|
||||||
|
visit poll_path(poll)
|
||||||
|
|
||||||
|
expect(page).to have_link('Han Solo')
|
||||||
|
expect(page).to have_link('Chewbacca')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Level 2 users reading a all-geozones question' do
|
||||||
|
create(:poll_question, poll: poll, all_geozones: true, valid_answers: 'Han Solo, Chewbacca')
|
||||||
|
login_as(create(:user, :level_two, geozone: geozone))
|
||||||
|
visit poll_path(poll)
|
||||||
|
|
||||||
|
expect(page).to have_link('Han Solo')
|
||||||
|
expect(page).to have_link('Chewbacca')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Level 2 users who have already answered' do
|
||||||
|
question = create(:poll_question, poll: poll, geozone_ids:[geozone.id], valid_answers: 'Han Solo, Chewbacca')
|
||||||
|
user = create(:user, :level_two, geozone: geozone)
|
||||||
|
create(:poll_partial_result, question: question, author: user, answer: 'Chewbacca')
|
||||||
|
login_as user
|
||||||
|
visit poll_path(poll)
|
||||||
|
|
||||||
|
expect(page).to have_link('Han Solo')
|
||||||
|
expect(page).to_not have_link('Chewbacca')
|
||||||
|
expect(page).to have_content('Chewbacca')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'Level 2 users answering', :js do
|
||||||
|
create(:poll_question, poll: poll, geozone_ids: [geozone.id], valid_answers: 'Han Solo, Chewbacca')
|
||||||
|
user = create(:user, :level_two, geozone: geozone)
|
||||||
|
login_as user
|
||||||
|
visit poll_path(poll)
|
||||||
|
|
||||||
|
click_link 'Han Solo'
|
||||||
|
|
||||||
|
expect(page).to_not have_link('Han Solo')
|
||||||
|
expect(page).to have_link('Chewbacca')
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
@@ -3,8 +3,9 @@ require 'cancan/matchers'
|
|||||||
|
|
||||||
describe "Abilities::Common" do
|
describe "Abilities::Common" do
|
||||||
subject(:ability) { Ability.new(user) }
|
subject(:ability) { Ability.new(user) }
|
||||||
|
let(:geozone) { create(:geozone) }
|
||||||
|
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user, geozone: geozone) }
|
||||||
|
|
||||||
let(:debate) { create(:debate) }
|
let(:debate) { create(:debate) }
|
||||||
let(:comment) { create(:comment) }
|
let(:comment) { create(:comment) }
|
||||||
@@ -13,6 +14,20 @@ describe "Abilities::Common" do
|
|||||||
let(:own_comment) { create(:comment, author: user) }
|
let(:own_comment) { create(:comment, author: user) }
|
||||||
let(:own_proposal) { create(:proposal, author: user) }
|
let(:own_proposal) { create(:proposal, author: user) }
|
||||||
|
|
||||||
|
let(:current_poll) { create(:poll) }
|
||||||
|
let(:incoming_poll) { create(:poll, :incoming) }
|
||||||
|
let(:expired_poll) { create(:poll, :expired) }
|
||||||
|
|
||||||
|
let(:poll_question_from_own_geozone) { create(:poll_question, geozones: [geozone]) }
|
||||||
|
let(:poll_question_from_other_geozone) { create(:poll_question, geozones: [create(:geozone)]) }
|
||||||
|
let(:poll_question_from_all_geozones) { create(:poll_question, all_geozones: true) }
|
||||||
|
let(:expired_poll_question_from_own_geozone) { create(:poll_question, poll: expired_poll, geozones: [geozone]) }
|
||||||
|
let(:expired_poll_question_from_other_geozone) { create(:poll_question, poll: expired_poll, geozones: [create(:geozone)]) }
|
||||||
|
let(:expired_poll_question_from_all_geozones) { create(:poll_question, poll: expired_poll, all_geozones: true) }
|
||||||
|
let(:incoming_poll_question_from_own_geozone) { create(:poll_question, poll: incoming_poll, geozones: [geozone]) }
|
||||||
|
let(:incoming_poll_question_from_other_geozone) { create(:poll_question, poll: incoming_poll, geozones: [create(:geozone)]) }
|
||||||
|
let(:incoming_poll_question_from_all_geozones) { create(:poll_question, poll: incoming_poll, all_geozones: true) }
|
||||||
|
|
||||||
it { should be_able_to(:index, Debate) }
|
it { should be_able_to(:index, Debate) }
|
||||||
it { should be_able_to(:show, debate) }
|
it { should be_able_to(:show, debate) }
|
||||||
it { should be_able_to(:vote, debate) }
|
it { should be_able_to(:vote, debate) }
|
||||||
@@ -103,6 +118,33 @@ describe "Abilities::Common" do
|
|||||||
it { should be_able_to(:create, DirectMessage) }
|
it { should be_able_to(:create, DirectMessage) }
|
||||||
it { should be_able_to(:show, own_direct_message) }
|
it { should be_able_to(:show, own_direct_message) }
|
||||||
it { should_not be_able_to(:show, create(:direct_message)) }
|
it { should_not be_able_to(:show, create(:direct_message)) }
|
||||||
|
|
||||||
|
it { should be_able_to(:answer, current_poll) }
|
||||||
|
it { should_not be_able_to(:answer, expired_poll) }
|
||||||
|
it { should_not be_able_to(:answer, incoming_poll) }
|
||||||
|
|
||||||
|
it { should be_able_to(:answer, poll_question_from_own_geozone ) }
|
||||||
|
it { should be_able_to(:answer, poll_question_from_all_geozones ) }
|
||||||
|
it { should_not be_able_to(:answer, poll_question_from_other_geozone ) }
|
||||||
|
it { should_not be_able_to(:answer, expired_poll_question_from_own_geozone ) }
|
||||||
|
it { should_not be_able_to(:answer, expired_poll_question_from_all_geozones ) }
|
||||||
|
it { should_not be_able_to(:answer, expired_poll_question_from_other_geozone ) }
|
||||||
|
it { should_not be_able_to(:answer, incoming_poll_question_from_own_geozone ) }
|
||||||
|
it { should_not be_able_to(:answer, incoming_poll_question_from_all_geozones ) }
|
||||||
|
it { should_not be_able_to(:answer, incoming_poll_question_from_other_geozone ) }
|
||||||
|
|
||||||
|
context "without geozone" do
|
||||||
|
before(:each) { user.geozone = nil }
|
||||||
|
it { should_not be_able_to(:answer, poll_question_from_own_geozone ) }
|
||||||
|
it { should be_able_to(:answer, poll_question_from_all_geozones ) }
|
||||||
|
it { should_not be_able_to(:answer, poll_question_from_other_geozone ) }
|
||||||
|
it { should_not be_able_to(:answer, expired_poll_question_from_own_geozone ) }
|
||||||
|
it { should_not be_able_to(:answer, expired_poll_question_from_all_geozones ) }
|
||||||
|
it { should_not be_able_to(:answer, expired_poll_question_from_other_geozone ) }
|
||||||
|
it { should_not be_able_to(:answer, incoming_poll_question_from_own_geozone ) }
|
||||||
|
it { should_not be_able_to(:answer, incoming_poll_question_from_all_geozones ) }
|
||||||
|
it { should_not be_able_to(:answer, incoming_poll_question_from_other_geozone ) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "when level 3 verified" do
|
describe "when level 3 verified" do
|
||||||
@@ -121,5 +163,32 @@ describe "Abilities::Common" do
|
|||||||
it { should be_able_to(:create, DirectMessage) }
|
it { should be_able_to(:create, DirectMessage) }
|
||||||
it { should be_able_to(:show, own_direct_message) }
|
it { should be_able_to(:show, own_direct_message) }
|
||||||
it { should_not be_able_to(:show, create(:direct_message)) }
|
it { should_not be_able_to(:show, create(:direct_message)) }
|
||||||
|
|
||||||
|
it { should be_able_to(:answer, current_poll) }
|
||||||
|
it { should_not be_able_to(:answer, expired_poll) }
|
||||||
|
it { should_not be_able_to(:answer, incoming_poll) }
|
||||||
|
|
||||||
|
it { should be_able_to(:answer, poll_question_from_own_geozone ) }
|
||||||
|
it { should be_able_to(:answer, poll_question_from_all_geozones ) }
|
||||||
|
it { should_not be_able_to(:answer, poll_question_from_other_geozone ) }
|
||||||
|
it { should_not be_able_to(:answer, expired_poll_question_from_own_geozone ) }
|
||||||
|
it { should_not be_able_to(:answer, expired_poll_question_from_all_geozones ) }
|
||||||
|
it { should_not be_able_to(:answer, expired_poll_question_from_other_geozone ) }
|
||||||
|
it { should_not be_able_to(:answer, incoming_poll_question_from_own_geozone ) }
|
||||||
|
it { should_not be_able_to(:answer, incoming_poll_question_from_all_geozones ) }
|
||||||
|
it { should_not be_able_to(:answer, incoming_poll_question_from_other_geozone ) }
|
||||||
|
|
||||||
|
context "without geozone" do
|
||||||
|
before(:each) { user.geozone = nil }
|
||||||
|
it { should_not be_able_to(:answer, poll_question_from_own_geozone ) }
|
||||||
|
it { should be_able_to(:answer, poll_question_from_all_geozones ) }
|
||||||
|
it { should_not be_able_to(:answer, poll_question_from_other_geozone ) }
|
||||||
|
it { should_not be_able_to(:answer, expired_poll_question_from_own_geozone ) }
|
||||||
|
it { should_not be_able_to(:answer, expired_poll_question_from_all_geozones ) }
|
||||||
|
it { should_not be_able_to(:answer, expired_poll_question_from_other_geozone ) }
|
||||||
|
it { should_not be_able_to(:answer, incoming_poll_question_from_own_geozone ) }
|
||||||
|
it { should_not be_able_to(:answer, incoming_poll_question_from_all_geozones ) }
|
||||||
|
it { should_not be_able_to(:answer, incoming_poll_question_from_other_geozone ) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
16
spec/models/poll/partial_result_spec.rb
Normal file
16
spec/models/poll/partial_result_spec.rb
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Poll::PartialResult, type: :model do
|
||||||
|
|
||||||
|
describe "validations" do
|
||||||
|
it "validates that the answers are included in the Enquiry's list" do
|
||||||
|
q = create(:poll_question, valid_answers: 'One, Two, Three')
|
||||||
|
expect(build(:poll_partial_result, question: q, answer: 'One')).to be_valid
|
||||||
|
expect(build(:poll_partial_result, question: q, answer: 'Two')).to be_valid
|
||||||
|
expect(build(:poll_partial_result, question: q, answer: 'Three')).to be_valid
|
||||||
|
|
||||||
|
expect(build(:poll_partial_result, question: q, answer: 'Four')).to_not be_valid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
28
spec/models/poll/question_spec.rb
Normal file
28
spec/models/poll/question_spec.rb
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Poll::Question, type: :model do
|
||||||
|
|
||||||
|
describe "#valid_answers" do
|
||||||
|
it "gets a comma-separated string, but returns an array" do
|
||||||
|
q = create(:poll_question, valid_answers: "Yes, No")
|
||||||
|
expect(q.valid_answers).to eq(["Yes", "No"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#copy_attributes_from_proposal" do
|
||||||
|
it "copies the attributes from the proposal" do
|
||||||
|
create_list(:geozone, 3)
|
||||||
|
p = create(:proposal)
|
||||||
|
q = create(:poll_question)
|
||||||
|
q.copy_attributes_from_proposal(p)
|
||||||
|
expect(q.valid_answers).to eq(['Yes', 'No'])
|
||||||
|
expect(q.author).to eq(p.author)
|
||||||
|
expect(q.author_visible_name).to eq(p.author.name)
|
||||||
|
expect(q.proposal_id).to eq(p.id)
|
||||||
|
expect(q.title).to eq(p.title)
|
||||||
|
expect(q.question).to eq(p.question)
|
||||||
|
expect(q.all_geozones).to be_true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user